Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-30 00:05:18 +04:00
parent 866f5927fa
commit e037233aa5
13 changed files with 557 additions and 8 deletions

View file

@ -9,6 +9,7 @@ import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
import com.tangem.domain.yield.supply.usecase.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -176,4 +177,16 @@ internal object YieldSupplyDomainModule {
currenciesRepository = currenciesRepository,
)
}
@Provides
@Singleton
fun provideYieldSupplyGetRewardsBalanceUseCase(
yieldSupplyRepository: YieldSupplyRepository,
dispatcherProvider: CoroutineDispatcherProvider,
): YieldSupplyGetRewardsBalanceUseCase {
return YieldSupplyGetRewardsBalanceUseCase(
yieldSupplyRepository = yieldSupplyRepository,
dispatcherProvider = dispatcherProvider,
)
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.core.ui.components.text
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import com.tangem.core.ui.res.TangemTheme
@Composable
fun TextAnimatedCounter(
text: String,
modifier: Modifier = Modifier,
style: TextStyle = TangemTheme.typography.caption1,
) {
var oldText by remember {
mutableStateOf(text)
}
SideEffect {
oldText = text
}
Row(modifier = modifier) {
for (i in text.indices) {
val oldChar = oldText.getOrNull(i)
val newChar = text[i]
val char = if (oldChar == newChar) {
oldText[i]
} else {
text[i]
}
AnimatedContent(
targetState = char,
transitionSpec = {
slideInVertically { it }.togetherWith(slideOutVertically { -it })
},
) { char ->
Text(
text = char.toString(),
style = style,
softWrap = false,
)
}
}
}
}

View file

@ -117,6 +117,24 @@ fun BigDecimalFiatFormat.price(): BigDecimalFormat = BigDecimalFormat { value ->
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
}
/**
* Formats fiat amount with an exact number of fractional digits.
*/
fun BigDecimalFiatFormat.anyDecimals(decimals: Int): BigDecimalFormat = BigDecimalFormat { value ->
val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
currency = formatterCurrency
maximumFractionDigits = decimals
minimumFractionDigits = decimals
isGroupingUsed = true
roundingMode = RoundingMode.HALF_UP
}
formatter.format(value)
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
}
// == Helpers ==
private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD

View file

@ -13,6 +13,9 @@ tasks.withType<Test>().configureEach {
}
dependencies {
/** Core */
implementation(projects.core.ui)
/** Domain */
implementation(projects.domain.models)
implementation(projects.domain.yieldSupply.models)
@ -23,6 +26,7 @@ dependencies {
implementation(projects.domain.blockaid)
implementation(projects.domain.quotes)
implementation(projects.domain.tokens)
implementation(projects.domain.appCurrency.models)
/** Tandem SDK */
implementation(tangemDeps.blockchain)

View file

@ -0,0 +1,99 @@
package com.tangem.domain.yield.supply.usecase
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.anyDecimals
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import java.math.BigDecimal
import java.math.RoundingMode
import kotlin.math.ceil
import kotlin.math.ln
class YieldSupplyGetRewardsBalanceUseCase(
private val yieldSupplyRepository: YieldSupplyRepository,
private val dispatcherProvider: CoroutineDispatcherProvider,
) {
operator fun invoke(status: CryptoCurrencyStatus, appCurrency: AppCurrency): Flow<String> = flow {
val amount: BigDecimal? = status.value.amount?.let { amt ->
status.value.fiatRate?.let { rate -> amt.multiply(rate) }
}
if (amount == null) return@flow
val tokenAddress = (status.currency as? CryptoCurrency.Token)?.contractAddress ?: return@flow
val apy = try {
val markets = yieldSupplyRepository.getCachedMarkets() ?: yieldSupplyRepository.updateMarkets()
markets.firstOrNull { it.tokenAddress.equals(tokenAddress, ignoreCase = true) }?.apy ?: BigDecimal.ZERO
} catch (_: Throwable) {
BigDecimal.ZERO
}
val apyFraction = apy.divide(HUNDRED_BD, SCALE, RoundingMode.HALF_UP)
if (apyFraction.compareTo(BigDecimal.ZERO) == 0) {
return@flow
}
val initialPerTickDelta = amount
.multiply(apyFraction)
.multiply(TICK_SECONDS_BD)
.divide(SECONDS_PER_YEAR_BD, SCALE, RoundingMode.HALF_UP)
.abs()
val minVisibleDecimals = calculateMinVisibleDecimals(initialPerTickDelta)
var currentBalance: BigDecimal = amount
while (true) {
emit(
currentBalance.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
).anyDecimals(decimals = minVisibleDecimals)
},
)
val perTickDelta = currentBalance
.multiply(apyFraction)
.multiply(TICK_SECONDS_BD)
.divide(SECONDS_PER_YEAR_BD, SCALE, RoundingMode.HALF_UP)
currentBalance = currentBalance.add(perTickDelta)
delay(TICK_MILLIS)
}
}.flowOn(dispatcherProvider.default)
private fun calculateMinVisibleDecimals(perTickDeltaAbs: BigDecimal): Int {
if (perTickDeltaAbs <= BigDecimal.ZERO) return MIN_DECIMALS
val perTickAsDouble = perTickDeltaAbs.toDouble()
if (perTickAsDouble.isNaN() || perTickAsDouble.isInfinite()) return MIN_DECIMALS
val safe = if (perTickAsDouble <= 0.0) EPSILON else perTickAsDouble
val raw = ceil(-ln(safe) / LN_10)
return raw.toInt().coerceIn(MIN_DECIMALS, MAX_DECIMALS)
}
private companion object {
const val TICK_MILLIS: Long = 300
private val TICK_SECONDS_BD = BigDecimal("0.3")
private val SECONDS_PER_YEAR_BD = BigDecimal("31536000") // 365 * 24 * 60 * 60
private val HUNDRED_BD = BigDecimal("100")
private const val SCALE = 18
private const val MIN_DECIMALS = 3
private const val MAX_DECIMALS = 8
private val LN_10 = ln(10.0)
private const val EPSILON = 1e-18
}
}

View file

@ -0,0 +1,301 @@
package com.tangem.domain.yield.supply.usecase
import com.google.common.truth.Truth.assertThat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.anyDecimals
import com.tangem.domain.appcurrency.model.AppCurrency
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.network.NetworkAddress
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import java.math.BigDecimal
import java.math.RoundingMode
import kotlin.math.ceil
import kotlin.math.ln
class YieldSupplyGetRewardsBalanceUseCaseTest {
private val repository: YieldSupplyRepository = mockk(relaxed = true)
@Test
fun `GIVEN null amount WHEN invoke THEN emit nothing`() = runTest {
val token = createToken(createNetwork())
val status = CryptoCurrencyStatus(
currency = token,
value = CryptoCurrencyStatus.Loading,
)
val dispatcherProvider = testDispatcherProvider(this)
val useCase = YieldSupplyGetRewardsBalanceUseCase(repository, dispatcherProvider)
val appCurrency = AppCurrency.Default
val emissions = useCase(status, appCurrency).toList()
assertThat(emissions).isEmpty()
}
@Test
fun `GIVEN coin currency WHEN invoke THEN emit nothing`() = runTest {
val network = createNetwork()
val coin = createNativeCoin(network)
val status = CryptoCurrencyStatus(
currency = coin,
value = CryptoCurrencyStatus.Custom(
amount = BigDecimal.ONE,
fiatAmount = null,
fiatRate = BigDecimal.ONE,
priceChange = null,
yieldBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = NetworkAddress.Single(
NetworkAddress.Address(value = "0xabc", type = NetworkAddress.Address.Type.Primary),
),
sources = CryptoCurrencyStatus.Sources(),
),
)
val dispatcherProvider = testDispatcherProvider(this)
val useCase = YieldSupplyGetRewardsBalanceUseCase(repository, dispatcherProvider)
val appCurrency = AppCurrency.Default
val emissions = useCase(status, appCurrency).toList()
assertThat(emissions).isEmpty()
}
@Test
fun `GIVEN zero apy WHEN invoke THEN emit nothing`() = runTest {
val network = createNetwork()
val token = createToken(network)
val amount = BigDecimal("123.45")
val status = CryptoCurrencyStatus(
currency = token,
value = CryptoCurrencyStatus.Custom(
amount = amount,
fiatAmount = null,
fiatRate = BigDecimal.ONE,
priceChange = null,
yieldBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = NetworkAddress.Single(
NetworkAddress.Address(value = "0xabc", type = NetworkAddress.Address.Type.Primary),
),
sources = CryptoCurrencyStatus.Sources(),
),
)
coEvery { repository.getCachedMarkets() } returns listOf(
YieldMarketToken(
tokenAddress = token.contractAddress,
chainId = 1,
apy = BigDecimal.ZERO,
isActive = true,
maxFeeNative = "0",
maxFeeUSD = "0",
backendId = "id",
),
)
val dispatcherProvider = testDispatcherProvider(this)
val useCase = YieldSupplyGetRewardsBalanceUseCase(repository, dispatcherProvider)
val appCurrency = AppCurrency.Default
val emissions = useCase(status, appCurrency).toList()
assertThat(emissions).isEmpty()
}
@Test
fun `GIVEN positive apy WHEN invoke THEN emit growing formatted balances`() = runTest {
val network = createNetwork()
val token = createToken(network)
val amount = BigDecimal("100.0")
val apy = BigDecimal("12.0") // 12%
val status = CryptoCurrencyStatus(
currency = token,
value = CryptoCurrencyStatus.Custom(
amount = amount,
fiatAmount = null,
fiatRate = BigDecimal.ONE,
priceChange = null,
yieldBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = NetworkAddress.Single(
NetworkAddress.Address(value = "0xabc", type = NetworkAddress.Address.Type.Primary),
),
sources = CryptoCurrencyStatus.Sources(),
),
)
coEvery { repository.getCachedMarkets() } returns listOf(
YieldMarketToken(
tokenAddress = token.contractAddress,
chainId = 1,
apy = apy,
isActive = true,
maxFeeNative = "0",
maxFeeUSD = "0",
backendId = "id",
),
)
val dispatcherProvider = testDispatcherProvider(this)
val useCase = YieldSupplyGetRewardsBalanceUseCase(repository, dispatcherProvider)
val appCurrency = AppCurrency.Default
val deferred = async { useCase(status, appCurrency).take(3).toList() }
testScheduler.advanceUntilIdle()
advanceTimeBy(300)
testScheduler.advanceUntilIdle()
advanceTimeBy(300)
testScheduler.advanceUntilIdle()
val collected = deferred.await()
assertThat(collected).hasSize(3)
val expectedDecimals = calculateMinVisibleDecimalsForTest(initialPerTickDelta(amount, apy))
val firstExpected = amount.format { fiat(
appCurrency.code,
appCurrency.symbol,
).anyDecimals(decimals = expectedDecimals) }
assertThat(collected[0]).isEqualTo(firstExpected)
val firstNext = nextBalance(amount, apy)
val secondExpected = firstNext.format { fiat(
appCurrency.code,
appCurrency.symbol,
).anyDecimals(decimals = expectedDecimals) }
assertThat(collected[1]).isEqualTo(secondExpected)
val secondNext = nextBalance(firstNext, apy)
val thirdExpected = secondNext.format { fiat(
appCurrency.code,
appCurrency.symbol,
).anyDecimals(decimals = expectedDecimals) }
assertThat(collected[2]).isEqualTo(thirdExpected)
}
private fun testDispatcherProvider(scope: TestScope): CoroutineDispatcherProvider {
val dispatcher: CoroutineDispatcher = StandardTestDispatcher(scope.testScheduler)
return object : CoroutineDispatcherProvider {
override val main: CoroutineDispatcher = dispatcher
override val mainImmediate: CoroutineDispatcher = dispatcher
override val io: CoroutineDispatcher = dispatcher
override val default: CoroutineDispatcher = dispatcher
override val single: CoroutineDispatcher = dispatcher
}
}
private fun createNetwork(): Network {
val derivationPath = Network.DerivationPath.None
return Network(
id = Network.ID(Network.RawID("polygon"), derivationPath),
backendId = "polygon",
name = "Polygon",
currencySymbol = "MATIC",
derivationPath = derivationPath,
isTestnet = false,
standardType = Network.StandardType.ERC20,
hasFiatFeeRate = false,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.ENS,
)
}
private fun createNativeCoin(network: Network): CryptoCurrency.Coin {
val nativeCoinId = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(network.rawId),
suffix = CryptoCurrency.ID.Suffix.RawID("polygon-ecosystem-token"),
)
return CryptoCurrency.Coin(
id = nativeCoinId,
network = network,
name = "Polygon",
symbol = "MATIC",
decimals = 18,
iconUrl = null,
isCustom = false,
)
}
private fun createToken(network: Network): CryptoCurrency.Token {
val tokenId = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(network.rawId),
suffix = CryptoCurrency.ID.Suffix.RawID("test-token", "0xContract"),
)
return CryptoCurrency.Token(
id = tokenId,
network = network,
name = "Test Token",
symbol = "TT",
decimals = 18,
iconUrl = null,
isCustom = false,
contractAddress = "0xContract",
)
}
private fun initialPerTickDelta(amount: BigDecimal, apy: BigDecimal): BigDecimal {
val apyFraction = apy.divide(HUNDRED_BD, SCALE, RoundingMode.HALF_UP)
return amount
.multiply(apyFraction)
.multiply(TICK_SECONDS_BD)
.divide(SECONDS_PER_YEAR_BD, SCALE, RoundingMode.HALF_UP)
.abs()
}
private fun nextBalance(current: BigDecimal, apy: BigDecimal): BigDecimal {
val apyFraction = apy.divide(HUNDRED_BD, SCALE, RoundingMode.HALF_UP)
val perTickDelta = current
.multiply(apyFraction)
.multiply(TICK_SECONDS_BD)
.divide(SECONDS_PER_YEAR_BD, SCALE, RoundingMode.HALF_UP)
return current.add(perTickDelta)
}
private fun calculateMinVisibleDecimalsForTest(perTickDeltaAbs: BigDecimal): Int {
if (perTickDeltaAbs <= BigDecimal.ZERO) return MIN_DECIMALS
val perTickAsDouble = perTickDeltaAbs.toDouble()
if (perTickAsDouble.isNaN() || perTickAsDouble.isInfinite()) return MIN_DECIMALS
val safe = if (perTickAsDouble <= 0.0) EPSILON else perTickAsDouble
val raw = ceil(-ln(safe) / LN_10)
return raw.toInt().coerceIn(MIN_DECIMALS, MAX_DECIMALS)
}
private companion object {
private const val SCALE = 18
private val TICK_SECONDS_BD = BigDecimal("0.3")
private val SECONDS_PER_YEAR_BD = BigDecimal("31536000")
private val HUNDRED_BD = BigDecimal("100")
private const val MIN_DECIMALS = 3
private const val MAX_DECIMALS = 8
private val LN_10 = ln(10.0)
private const val EPSILON = 1e-18
}
}

View file

@ -87,6 +87,7 @@ dependencies {
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.yieldSupply)
/** Temp dependency to swap domain */
implementation(projects.features.swap.domain)

View file

@ -121,6 +121,7 @@ internal object TokenDetailsPreviewData {
selectedBalanceType = BalanceType.ALL,
onBalanceSelect = {},
displayCryptoBalance = "966,96 XLM",
displayYeildSupplyCryptoBalance = null,
displayFiatBalance = "91,50$",
isBalanceSelectorEnabled = true,
isBalanceFlickering = false,

View file

@ -40,6 +40,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TokenReceiveNotification
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
@ -73,6 +74,7 @@ import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetRewardsBalanceUseCase
import com.tangem.feature.tokendetails.deeplink.TokenDetailsDeepLinkActionListener
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsCurrencyStatusAnalyticsSender
@ -150,6 +152,7 @@ internal class TokenDetailsModel @Inject constructor(
private val accountsFeatureToggles: AccountsFeatureToggles,
private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val yieldSupplyGetRewardsBalanceUseCase: YieldSupplyGetRewardsBalanceUseCase,
) : Model(), TokenDetailsClickIntents, YieldSupplyDepositedWarningComponent.ModelCallback {
private val params = paramsContainer.require<TokenDetailsComponent.Params>()
@ -164,6 +167,7 @@ internal class TokenDetailsModel @Inject constructor(
private val expressTxJobHolder = JobHolder()
private val buttonsJobHolder = JobHolder()
private val stakingJobHolder = JobHolder()
private val yieldSupplyBalanceJobHolder = JobHolder()
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
@ -350,6 +354,7 @@ internal class TokenDetailsModel @Inject constructor(
updateButtons(currencyStatus = status)
updateWarnings(status)
subscribeOnUpdateStakingInfo(status)
subscribeOnYieldSupplyBalanceIfActive(status)
}
currencyStatusAnalyticsSender.send(maybeCurrencyStatus)
}
@ -393,6 +398,26 @@ internal class TokenDetailsModel @Inject constructor(
.saveIn(expressTxJobHolder)
}
private fun subscribeOnYieldSupplyBalanceIfActive(status: CryptoCurrencyStatus) {
if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled &&
status.value.yieldSupplyStatus?.isActive == true
) {
if (yieldSupplyBalanceJobHolder.isActive && status.value.sources.networkSource != StatusSource.ACTUAL) {
return
}
yieldSupplyGetRewardsBalanceUseCase(status = status, appCurrency = selectedAppCurrencyFlow.value)
.onEach { formatted ->
internalUiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance(formatted)
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
.saveIn(yieldSupplyBalanceJobHolder)
} else {
yieldSupplyBalanceJobHolder.cancel()
internalUiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance(null)
}
}
private fun updateNetworkToSwapBalance(toCryptoCurrency: CryptoCurrency) {
modelScope.launch {
updateDelayedCurrencyStatusUseCase(

View file

@ -28,6 +28,7 @@ internal sealed class TokenDetailsBalanceBlockState {
val isBalanceSelectorEnabled: Boolean,
val isBalanceFlickering: Boolean,
val yieldSupplyState: TokenDetailsYieldSupplyState = TokenDetailsYieldSupplyState.Empty,
val displayYeildSupplyCryptoBalance: String? = null,
) : TokenDetailsBalanceBlockState()
data class Error(

View file

@ -98,6 +98,8 @@ internal class TokenDetailsLoadedBalanceConverter(
stakingCryptoAmount,
currentState.selectedBalanceType,
),
displayYeildSupplyCryptoBalance = (currentState as? TokenDetailsBalanceBlockState.Content)
?.displayYeildSupplyCryptoBalance,
balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig,
onBalanceSelect = clickIntents::onBalanceSelect,
selectedBalanceType = currentState.selectedBalanceType,

View file

@ -29,6 +29,7 @@ import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
import com.tangem.features.tokendetails.impl.R
@ -314,6 +315,19 @@ internal class TokenDetailsStateFactory(
return balanceSelectStateConverter.convert(buttonConfig)
}
fun getStateWithUpdatedYieldSupplyDisplayBalance(displayBalance: String?): TokenDetailsState {
val state = currentStateProvider()
val balanceState = state.tokenBalanceBlockState
return state.copy(
tokenBalanceBlockState = when (balanceState) {
is TokenDetailsBalanceBlockState.Content ->
balanceState.copy(displayYeildSupplyCryptoBalance = displayBalance)
is TokenDetailsBalanceBlockState.Error -> balanceState
is TokenDetailsBalanceBlockState.Loading -> balanceState
},
)
}
fun getStateWithConfirmHideExpressStatus(): TokenDetailsState {
return currentStateProvider().copy(
dialogConfig = TokenDetailsDialogConfig(

View file

@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.buttons.HorizontalActionChips
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.text.TextAnimatedCounter
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
@ -121,14 +122,29 @@ private fun FiatBalance(
height = TangemTheme.dimens.size32,
),
)
is TokenDetailsBalanceBlockState.Content -> Text(
modifier = modifier,
text = state.displayFiatBalance.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.h2.applyBladeBrush(
isEnabled = state.isBalanceFlickering,
textColor = TangemTheme.colors.text.primary1,
),
)
is TokenDetailsBalanceBlockState.Content -> if (state.displayYeildSupplyCryptoBalance != null &&
!isBalanceHidden
) {
TextAnimatedCounter(
modifier = modifier,
text = state.displayYeildSupplyCryptoBalance,
style = TangemTheme.typography.h2.applyBladeBrush(
isEnabled = state.isBalanceFlickering,
textColor = TangemTheme.colors.text.primary1,
),
)
} else {
Text(
modifier = modifier,
text = (state.displayYeildSupplyCryptoBalance ?: state.displayFiatBalance).orMaskWithStars(
isBalanceHidden,
),
style = TangemTheme.typography.h2.applyBladeBrush(
isEnabled = state.isBalanceFlickering,
textColor = TangemTheme.colors.text.primary1,
),
)
}
is TokenDetailsBalanceBlockState.Error -> Text(
modifier = modifier,
text = DASH_SIGN.orMaskWithStars(isBalanceHidden),