From dc097a7815648ff54d6d1764ad1b2e09cc2a21d5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 6 Mar 2025 14:21:14 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../data/staking/DefaultStakingRepository.kt | 74 +++++--- .../staking/GetStakingAvailabilityUseCase.kt | 23 ++- .../staking/repositories/StakingRepository.kt | 2 +- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 19 +- .../repository/MockStakingRepository.kt | 4 +- .../TokenDetailsLoadedBalanceConverter.kt | 137 +------------- .../TokenDetailsStakingInfoConverter.kt | 174 ++++++++++++++++++ .../state/factory/TokenDetailsStateFactory.kt | 18 +- .../viewmodels/TokenDetailsViewModel.kt | 48 ++--- 9 files changed, 288 insertions(+), 211 deletions(-) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index ff2c188133..2bca8708d4 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -59,11 +59,8 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.lib.crypto.BlockchainUtils.isSolana import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.plus -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeoutOrNull import timber.log.Timber import kotlin.time.Duration.Companion.seconds @@ -186,32 +183,48 @@ internal class DefaultStakingRepository( } } - override suspend fun getStakingAvailability( + override fun getStakingAvailability( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): StakingAvailability { - if (!checkFeatureToggleEnabled(cryptoCurrency.network.id)) return StakingAvailability.Unavailable - - if (checkForInvalidCardBatch(userWalletId, cryptoCurrency)) return StakingAvailability.Unavailable - - val rawCurrencyId = cryptoCurrency.id.rawCurrencyId ?: return StakingAvailability.Unavailable - - val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not() - - val prefetchedYield = findPrefetchedYield( - yields = getEnabledYieldsSync(), - currencyId = rawCurrencyId, - symbol = cryptoCurrency.symbol, - ) - - return when { - prefetchedYield != null && isSupportedInMobileApp -> { - StakingAvailability.Available(prefetchedYield.id) + ): Flow { + return channelFlow { + if (!checkFeatureToggleEnabled(cryptoCurrency.network.id)) { + send(StakingAvailability.Unavailable) + return@channelFlow } - prefetchedYield == null && isSupportedInMobileApp -> { - StakingAvailability.TemporaryUnavailable + + if (checkForInvalidCardBatch(userWalletId, cryptoCurrency)) { + send(StakingAvailability.Unavailable) + return@channelFlow } - else -> StakingAvailability.Unavailable + + val rawCurrencyId = cryptoCurrency.id.rawCurrencyId + if (rawCurrencyId == null) { + send(StakingAvailability.Unavailable) + return@channelFlow + } + + val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not() + + getEnabledYields() + .distinctUntilChanged() + .onEach { yields -> + val prefetchedYield = findPrefetchedYield( + yields = yields, + currencyId = rawCurrencyId, + symbol = cryptoCurrency.symbol, + ) + when { + prefetchedYield != null && isSupportedInMobileApp -> { + send(StakingAvailability.Available(prefetchedYield.id)) + } + prefetchedYield == null && isSupportedInMobileApp -> { + send(StakingAvailability.TemporaryUnavailable) + } + else -> send(StakingAvailability.Unavailable) + } + } + .launchIn(this) } } @@ -633,6 +646,15 @@ internal class DefaultStakingRepository( ) } + private fun getEnabledYields(): Flow> { + return stakingYieldsStore.get().map { + YieldConverter.convertListIgnoreErrors( + input = it, + onError = { Timber.e("Error converting one of the items in enabled yields: $it") }, + ) + } + } + private fun getBalanceRequestData(address: String, integrationId: String): YieldBalanceRequestBody { return YieldBalanceRequestBody( addresses = Address( diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt index 7133219a03..dcabaa8bca 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt @@ -1,12 +1,17 @@ package com.tangem.domain.staking import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map /** * Use case for getting info about staking capability in tangem app. @@ -16,17 +21,15 @@ class GetStakingAvailabilityUseCase( private val stakingErrorResolver: StakingErrorResolver, ) { - suspend operator fun invoke( + operator fun invoke( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Either { - return Either - .catch { - stakingRepository.getStakingAvailability( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrency, - ) - } - .mapLeft { stakingErrorResolver.resolve(it) } + ): EitherFlow { + return stakingRepository.getStakingAvailability( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ).map> { + it.right() + }.catch { emit(stakingErrorResolver.resolve(it).left()) } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index ffd12cda37..b583c5f491 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -35,7 +35,7 @@ interface StakingRepository { suspend fun getYield(yieldId: String): Yield - suspend fun getStakingAvailability(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): StakingAvailability + fun getStakingAvailability(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow suspend fun getActions( userWalletId: UserWalletId, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index c01b86587d..cabe3d43f7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -60,13 +60,18 @@ class GetCryptoCurrencyActionsUseCase( val flow = combine( flow = networkFlow, flow2 = promoRepository.getStoryById(StoryContentIds.STORY_FIRST_TIME_SWAP.id).conflate(), - ) { maybeCoinStatus, maybeSwapStories -> + flow3 = stakingRepository.getStakingAvailability( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + ).onStart { emit(StakingAvailability.Unavailable) }, + ) { maybeCoinStatus, maybeSwapStories, stakingAvailability -> createTokenActionsState( userWallet = userWallet, coinStatus = maybeCoinStatus.getOrNull(), cryptoCurrencyStatus = cryptoCurrencyStatus, requirements = requirements, shouldShowSwapStories = maybeSwapStories != null, + isStakingAvailable = stakingAvailability is StakingAvailability.Available, ) } @@ -80,6 +85,7 @@ class GetCryptoCurrencyActionsUseCase( cryptoCurrencyStatus: CryptoCurrencyStatus, requirements: AssetRequirementsCondition?, shouldShowSwapStories: Boolean, + isStakingAvailable: Boolean, ): TokenActionsState { return TokenActionsState( walletId = userWallet.walletId, @@ -90,6 +96,7 @@ class GetCryptoCurrencyActionsUseCase( cryptoCurrencyStatus = cryptoCurrencyStatus, requirements = requirements, shouldShowSwapStories = shouldShowSwapStories, + isStakingAvailable = isStakingAvailable, ), ) } @@ -105,6 +112,7 @@ class GetCryptoCurrencyActionsUseCase( cryptoCurrencyStatus: CryptoCurrencyStatus, requirements: AssetRequirementsCondition?, shouldShowSwapStories: Boolean, + isStakingAvailable: Boolean, ): List { val cryptoCurrency = cryptoCurrencyStatus.currency if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation) { @@ -139,7 +147,7 @@ class GetCryptoCurrencyActionsUseCase( } // staking - if (isStakingAvailable(userWallet, cryptoCurrency)) { + if (isStakingAvailable) { val yield = kotlin.runCatching { stakingRepository.getYield( cryptoCurrencyId = cryptoCurrency.id, @@ -354,13 +362,6 @@ class GetCryptoCurrencyActionsUseCase( return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty() } - private suspend fun isStakingAvailable(userWallet: UserWallet, cryptoCurrency: CryptoCurrency): Boolean { - return stakingRepository.getStakingAvailability( - userWalletId = userWallet.walletId, - cryptoCurrency = cryptoCurrency, - ) is StakingAvailability.Available - } - private companion object { const val REQUEST_EXCHANGE_DATA_TIMEOUT = 1000L } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index 37fe931195..2b63da6411 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -43,10 +43,10 @@ class MockStakingRepository : StakingRepository { override suspend fun getYield(yieldId: String) = yield - override suspend fun getStakingAvailability( + override fun getStakingAvailability( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): StakingAvailability = StakingAvailability.Unavailable + ): Flow = flowOf(StakingAvailability.Unavailable) override suspend fun getActions( userWalletId: UserWalletId, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 1c1e455117..131b0eb114 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -6,16 +6,9 @@ import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource -import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -24,9 +17,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents -import com.tangem.features.tokendetails.impl.R -import com.tangem.lib.crypto.BlockchainUtils.isBSC -import com.tangem.lib.crypto.BlockchainUtils.isSolana import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter @@ -39,8 +29,6 @@ import java.math.BigDecimal internal class TokenDetailsLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, - private val stakingEntryInfoProvider: Provider, - private val stakingAvailabilityProvider: Provider, private val symbol: String, private val decimals: Int, private val clickIntents: TokenDetailsClickIntents, @@ -80,7 +68,7 @@ internal class TokenDetailsLoadedBalanceConverter( currentState = state.tokenBalanceBlockState, status = status, ), - stakingBlocksState = getYieldBalance(status, state), + stakingBlocksState = state.stakingBlocksState, marketPriceBlockState = getMarketPriceState(status = status.value, currencySymbol = currencyName), pendingTxs = pendingTxs, txHistoryState = if (state.txHistoryState is TxHistoryState.NotSupported) { @@ -140,58 +128,6 @@ internal class TokenDetailsLoadedBalanceConverter( } } - private fun getYieldBalance(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM? { - return when (stakingAvailabilityProvider.invoke()) { - StakingAvailability.TemporaryUnavailable -> StakingBlockUM.TemporaryUnavailable - StakingAvailability.Unavailable -> null - is StakingAvailability.Available -> getStakingInfoBlock(status, state) - } - } - - private fun getStakingInfoBlock(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM? { - val yieldBalance = status.value.yieldBalance as? YieldBalance.Data - - val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance() - val pendingBalances = yieldBalance?.balance?.items ?: emptyList() - - val stakingEntryInfo = stakingEntryInfoProvider.invoke() - val iconState = state.tokenInfoBlockState.iconState - - return when { - stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> { - if (pendingBalances.isEmpty()) { - getStakeAvailableState(stakingEntryInfo, iconState, isStakingButtonEnabled(status)) - } else { - getStakedBlockWithFiatAmount(status, pendingBalances.sumOf { it.amount }, null) - } - } - stakingCryptoAmount.isNullOrZero() && stakingEntryInfo == null -> { - null - } - else -> getStakedBlockWithFiatAmount(status, stakingCryptoAmount, yieldBalance?.getRewardStakingBalance()) - } - } - - private fun isStakingButtonEnabled(status: CryptoCurrencyStatus): Boolean { - return status.value is CryptoCurrencyStatus.Loaded || - status.value is CryptoCurrencyStatus.NoQuote || - status.value is CryptoCurrencyStatus.Custom - } - - private fun getStakedBlockWithFiatAmount( - status: CryptoCurrencyStatus, - stakingAmount: BigDecimal?, - rewardAmount: BigDecimal?, - ): StakingBlockUM.Staked { - val fiatRate = status.value.fiatRate - return getStakedState( - status = status, - stakingCryptoAmount = stakingAmount, - stakingFiatAmount = stakingAmount?.let { fiatRate?.multiply(it) }, - stakingRewardAmount = rewardAmount?.let { fiatRate?.multiply(it) }, - ) - } - private fun getMarketPriceState( status: CryptoCurrencyStatus.Value, currencySymbol: String, @@ -215,52 +151,6 @@ internal class TokenDetailsLoadedBalanceConverter( } } - private fun getStakeAvailableState( - stakingEntryInfo: StakingEntryInfo, - iconState: IconState, - isEnabled: Boolean, - ): StakingBlockUM.StakeAvailable { - val apr = stakingEntryInfo.apr.format { percent() } - return StakingBlockUM.StakeAvailable( - titleText = resourceReference( - id = R.string.token_details_staking_block_title, - formatArgs = wrappedList(apr), - ), - subtitleText = resourceReference( - id = R.string.staking_notification_earn_rewards_text, - formatArgs = wrappedList(stakingEntryInfo.tokenSymbol), - ), - iconState = iconState, - isEnabled = isEnabled, - onStakeClicked = clickIntents::onStakeBannerClick, - ) - } - - private fun getStakedState( - status: CryptoCurrencyStatus, - stakingCryptoAmount: BigDecimal?, - stakingFiatAmount: BigDecimal?, - stakingRewardAmount: BigDecimal?, - ): StakingBlockUM.Staked { - return StakingBlockUM.Staked( - cryptoAmount = stakingCryptoAmount, - fiatAmount = stakingFiatAmount, - cryptoValue = stringReference( - stakingCryptoAmount.format { crypto(symbol = symbol, decimals = decimals) }, - ), - fiatValue = stringReference( - stakingFiatAmount.format { - fiat( - appCurrencyProvider().code, - appCurrencyProvider().symbol, - ) - }, - ), - rewardValue = getRewardText(status, stakingRewardAmount), - onStakeClicked = clickIntents::onStakeBannerClick, - ) - } - private fun CryptoCurrencyStatus.Value.toContentConfig(currencySymbol: String): MarketPriceBlockState.Content { return MarketPriceBlockState.Content( currencySymbol = currencySymbol, @@ -321,31 +211,6 @@ internal class TokenDetailsLoadedBalanceConverter( return totalAmount.format { crypto(status.currency) } } - private fun getRewardText(status: CryptoCurrencyStatus, stakingRewardAmount: BigDecimal?): TextReference { - val blockchainId = status.currency.network.id.value - val rewardBlockType = when { - isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable - stakingRewardAmount.isNullOrZero() -> RewardBlockType.NoRewards - else -> RewardBlockType.Rewards - } - - return when (rewardBlockType) { - RewardBlockType.Rewards -> resourceReference( - R.string.staking_details_rewards_to_claim, - wrappedList( - stakingRewardAmount.format { - fiat( - appCurrencyProvider().code, - appCurrencyProvider().symbol, - ) - }, - ), - ) - RewardBlockType.NoRewards -> resourceReference(R.string.staking_details_no_rewards_to_claim) - RewardBlockType.RewardUnavailable -> TextReference.EMPTY - } - } - private fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = getStatusSource() == StatusSource.CACHE private fun CryptoCurrencyStatus.Value.getStatusSource(): StatusSource? { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt new file mode 100644 index 0000000000..01c5bcb827 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -0,0 +1,174 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.stakekit.RewardBlockType +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.tokendetails.impl.R +import com.tangem.lib.crypto.BlockchainUtils.isBSC +import com.tangem.lib.crypto.BlockchainUtils.isSolana +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero +import java.math.BigDecimal + +internal class TokenDetailsStakingInfoConverter( + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + private val clickIntents: TokenDetailsClickIntents, + private val currentState: TokenDetailsState, + private val stakingEntryInfo: StakingEntryInfo?, +) : Converter { + + override fun convert(value: StakingAvailability): TokenDetailsState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider.invoke() ?: return currentState + return currentState.copy( + stakingBlocksState = getYieldBalance(cryptoCurrencyStatus, currentState, value), + ) + } + + private fun getYieldBalance( + status: CryptoCurrencyStatus, + state: TokenDetailsState, + stakingAvailability: StakingAvailability, + ): StakingBlockUM? { + return when (stakingAvailability) { + StakingAvailability.TemporaryUnavailable -> StakingBlockUM.TemporaryUnavailable + StakingAvailability.Unavailable -> null + is StakingAvailability.Available -> getStakingInfoBlock(status, state) + } + } + + private fun getStakingInfoBlock(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM? { + val yieldBalance = status.value.yieldBalance as? YieldBalance.Data + + val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance() + val pendingBalances = yieldBalance?.balance?.items ?: emptyList() + + val iconState = state.tokenInfoBlockState.iconState + + return when { + stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> { + if (pendingBalances.isEmpty()) { + getStakeAvailableState(stakingEntryInfo, iconState, isStakingButtonEnabled(status)) + } else { + getStakedBlockWithFiatAmount(status, pendingBalances.sumOf { it.amount }, null) + } + } + stakingCryptoAmount.isNullOrZero() && stakingEntryInfo == null -> { + null + } + else -> getStakedBlockWithFiatAmount(status, stakingCryptoAmount, yieldBalance?.getRewardStakingBalance()) + } + } + + private fun isStakingButtonEnabled(status: CryptoCurrencyStatus): Boolean { + return status.value is CryptoCurrencyStatus.Loaded || + status.value is CryptoCurrencyStatus.NoQuote || + status.value is CryptoCurrencyStatus.Custom + } + + private fun getStakedBlockWithFiatAmount( + status: CryptoCurrencyStatus, + stakingAmount: BigDecimal?, + rewardAmount: BigDecimal?, + ): StakingBlockUM.Staked { + val fiatRate = status.value.fiatRate + return getStakedState( + status = status, + stakingCryptoAmount = stakingAmount, + stakingFiatAmount = stakingAmount?.let { fiatRate?.multiply(it) }, + stakingRewardAmount = rewardAmount?.let { fiatRate?.multiply(it) }, + ) + } + + private fun getStakeAvailableState( + stakingEntryInfo: StakingEntryInfo, + iconState: IconState, + isEnabled: Boolean, + ): StakingBlockUM.StakeAvailable { + val apr = stakingEntryInfo.apr.format { percent() } + return StakingBlockUM.StakeAvailable( + titleText = resourceReference( + id = R.string.token_details_staking_block_title, + formatArgs = wrappedList(apr), + ), + subtitleText = resourceReference( + id = R.string.staking_notification_earn_rewards_text, + formatArgs = wrappedList(stakingEntryInfo.tokenSymbol), + ), + iconState = iconState, + isEnabled = isEnabled, + onStakeClicked = clickIntents::onStakeBannerClick, + ) + } + + private fun getStakedState( + status: CryptoCurrencyStatus, + stakingCryptoAmount: BigDecimal?, + stakingFiatAmount: BigDecimal?, + stakingRewardAmount: BigDecimal?, + ): StakingBlockUM.Staked { + return StakingBlockUM.Staked( + cryptoAmount = stakingCryptoAmount, + fiatAmount = stakingFiatAmount, + cryptoValue = stringReference( + stakingCryptoAmount.format { + crypto( + symbol = status.currency.symbol, + decimals = status.currency.decimals, + ) + }, + ), + fiatValue = stringReference( + stakingFiatAmount.format { + fiat( + appCurrencyProvider().code, + appCurrencyProvider().symbol, + ) + }, + ), + rewardValue = getRewardText(status, stakingRewardAmount), + onStakeClicked = clickIntents::onStakeBannerClick, + ) + } + + private fun getRewardText(status: CryptoCurrencyStatus, stakingRewardAmount: BigDecimal?): TextReference { + val blockchainId = status.currency.network.id.value + val rewardBlockType = when { + isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable + stakingRewardAmount.isNullOrZero() -> RewardBlockType.NoRewards + else -> RewardBlockType.Rewards + } + + return when (rewardBlockType) { + RewardBlockType.Rewards -> resourceReference( + R.string.staking_details_rewards_to_claim, + wrappedList( + stakingRewardAmount.format { + fiat( + appCurrencyProvider().code, + appCurrencyProvider().symbol, + ) + }, + ), + ) + RewardBlockType.NoRewards -> resourceReference(R.string.staking_details_no_rewards_to_claim) + RewardBlockType.RewardUnavailable -> TextReference.EMPTY + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index e88c1cc061..1ee5772d32 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -44,8 +44,6 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, - private val stakingEntryInfoProvider: Provider, - private val stakingAvailabilityProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, private val clickIntents: TokenDetailsClickIntents, private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, @@ -74,8 +72,6 @@ internal class TokenDetailsStateFactory( TokenDetailsLoadedBalanceConverter( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, - stakingEntryInfoProvider = stakingEntryInfoProvider, - stakingAvailabilityProvider = stakingAvailabilityProvider, symbol = symbol, decimals = decimals, clickIntents = clickIntents, @@ -129,6 +125,20 @@ internal class TokenDetailsStateFactory( return tokenDetailsLoadedBalanceConverter.convert(cryptoCurrencyEither) } + fun getStakingInfoState( + state: TokenDetailsState, + stakingEntryInfo: StakingEntryInfo?, + stakingAvailability: StakingAvailability, + ): TokenDetailsState { + return TokenDetailsStakingInfoConverter( + currentState = state, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + stakingEntryInfo = stakingEntryInfo, + ).convert(stakingAvailability) + } + fun getManageButtonsState(actions: List): TokenDetailsState { return tokenDetailsButtonsConverter.convert(actions) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index d779c92d75..ed991eda47 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -43,7 +43,6 @@ import com.tangem.domain.staking.GetStakingEntryInfoUseCase import com.tangem.domain.staking.GetStakingIntegrationIdUseCase import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency @@ -149,18 +148,16 @@ internal class TokenDetailsViewModel @Inject constructor( private val refreshStateJobHolder = JobHolder() private val warningsJobHolder = JobHolder() private val expressTxJobHolder = JobHolder() + private val buttonsJobHolder = JobHolder() + private val stakingJobHolder = JobHolder() private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null - private var stakingEntryInfo: StakingEntryInfo? = null - private var stakingAvailability: StakingAvailability = StakingAvailability.Unavailable private var expressTxStatusTaskScheduler = SingleTaskScheduler>() private val stateFactory = TokenDetailsStateFactory( currentStateProvider = Provider { uiState.value }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), - stakingEntryInfoProvider = Provider { stakingEntryInfo }, - stakingAvailabilityProvider = Provider { stakingAvailability }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, clickIntents = this, networkHasDerivationUseCase = networkHasDerivationUseCase, @@ -263,8 +260,6 @@ internal class TokenDetailsViewModel @Inject constructor( subscribeOnCurrencyStatusUpdates() subscribeOnExpressTransactionsUpdates() updateTxHistory(refresh = false, showItemsLoading = true) - - updateStakingInfo() } private fun handleBalanceHiding(owner: LifecycleOwner) { @@ -290,6 +285,7 @@ internal class TokenDetailsViewModel @Inject constructor( } .flowOn(dispatchers.main) .launchIn(viewModelScope) + .saveIn(buttonsJobHolder) } private fun updateWarnings(cryptoCurrencyStatus: CryptoCurrencyStatus) { @@ -327,6 +323,7 @@ internal class TokenDetailsViewModel @Inject constructor( updateWarnings(status) } currencyStatusAnalyticsSender.send(maybeCurrencyStatus) + subscribeOnUpdateStakingInfo() } .flowOn(dispatchers.main) .launchIn(viewModelScope) @@ -412,24 +409,29 @@ internal class TokenDetailsViewModel @Inject constructor( } } - private fun updateStakingInfo() { - viewModelScope.launch { - val availability = getStakingAvailabilityUseCase( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrency, - ).getOrElse { StakingAvailability.Unavailable } + private fun subscribeOnUpdateStakingInfo() { + getStakingAvailabilityUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ) + .map { it.getOrElse { StakingAvailability.Unavailable } } + .distinctUntilChanged() + .onEach { + if (it is StakingAvailability.Available) { + val stakingInfo = getStakingEntryInfoUseCase( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ) - stakingAvailability = availability - - if (stakingAvailability is StakingAvailability.Available) { - val stakingInfo = getStakingEntryInfoUseCase( - cryptoCurrencyId = cryptoCurrency.id, - symbol = cryptoCurrency.symbol, - ) - - stakingEntryInfo = stakingInfo.getOrNull() + val stakingEntryInfo = stakingInfo.getOrNull() + internalUiState.update { state -> + stateFactory.getStakingInfoState(state, stakingEntryInfo, it) + } + } } - } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + .saveIn(stakingJobHolder) } private fun updateTopBarMenu() {