Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-16 12:18:53 +05:00
parent c003dbb39d
commit 9ea256e557
19 changed files with 166 additions and 9 deletions

View file

@ -1,10 +1,10 @@
package com.tangem.tap.di.domain
import com.tangem.domain.blockaid.BlockAidGasEstimate
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
@ -16,6 +16,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Suppress("TooManyFunctions")
@Module
@InstallIn(SingletonComponent::class)
internal object YieldSupplyDomainModule {
@ -225,4 +226,12 @@ internal object YieldSupplyDomainModule {
fun provideYieldSupplyGetDustMinAmountUseCase(): YieldSupplyGetDustMinAmountUseCase {
return YieldSupplyGetDustMinAmountUseCase()
}
@Provides
@Singleton
fun provideYieldSupplyGetAvailabilityUseCase(
yieldSupplyRepository: YieldSupplyRepository,
): YieldSupplyGetAvailabilityUseCase {
return YieldSupplyGetAvailabilityUseCase(yieldSupplyRepository)
}
}

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="40" android:viewportWidth="40" android:width="24dp">
<path android:fillColor="#000000" android:pathData="M15.849,26.896C16.954,26.896 17.849,27.728 17.85,28.753V33.615C17.849,34.641 16.954,35.473 15.849,35.473C14.745,35.473 13.85,34.641 13.849,33.615V28.753C13.85,27.727 14.745,26.896 15.849,26.896Z"/>
<path android:fillColor="#000000" android:pathData="M32.454,20.958C33.558,20.958 34.454,21.819 34.454,22.879V33.552C34.454,34.612 33.558,35.473 32.454,35.473C31.35,35.473 30.454,34.612 30.454,33.552V22.879C30.454,21.818 31.349,20.958 32.454,20.958Z"/>
<path android:fillColor="#000000" android:pathData="M7.547,29.467C8.651,29.467 9.547,30.362 9.547,31.467V33.472C9.547,34.576 8.651,35.472 7.547,35.472C6.442,35.472 5.547,34.576 5.547,33.472V31.467C5.547,30.362 6.442,29.467 7.547,29.467Z"/>
<path android:fillColor="#000000" android:pathData="M24.151,23.976C25.256,23.976 26.151,24.755 26.151,25.716V33.732C26.151,34.693 25.256,35.472 24.151,35.472C23.047,35.472 22.151,34.693 22.151,33.732V25.716C22.152,24.755 23.047,23.976 24.151,23.976Z"/>
<path android:fillColor="#000000" android:pathData="M29.46,4.062C29.927,4.036 30.392,4.174 30.77,4.457C31.203,4.781 31.486,5.266 31.556,5.801L32.671,14.396C32.813,15.491 32.04,16.495 30.944,16.637C29.849,16.779 28.846,16.006 28.704,14.911L28.228,11.251C26.87,12.973 25.156,14.988 23.389,16.644C19.165,20.601 14.718,22.22 9.224,21.976C8.12,21.927 7.265,20.992 7.314,19.889C7.363,18.786 8.298,17.931 9.401,17.98C13.802,18.175 17.202,16.958 20.653,13.725C22.214,12.263 23.781,10.43 25.071,8.796L20.395,9.536C19.305,9.709 18.28,8.965 18.107,7.874C17.935,6.784 18.679,5.759 19.769,5.586L29.259,4.083L29.46,4.062Z"/>
</vector>

View file

@ -13,6 +13,7 @@ dependencies {
api(projects.domain.core)
api(projects.domain.models)
api(projects.domain.wallets.models)
api(projects.domain.yieldSupply.models)
implementation(deps.arrow.core)
implementation(deps.kotlin.coroutines)

View file

@ -35,6 +35,7 @@ dependencies {
implementation(projects.domain.promo)
implementation(projects.domain.networks)
implementation(projects.domain.quotes)
implementation(projects.domain.yieldSupply.models)
/** Project - Api */
implementation(projects.features.staking.api)

View file

@ -17,6 +17,7 @@ import com.tangem.domain.tokens.actions.OutdatedDataActionsFactory
import com.tangem.domain.tokens.actions.UnreachableActionsFactory
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.models.YieldSupplyAvailability
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
@ -54,7 +55,11 @@ class GetCryptoCurrencyActionsUseCase(
)
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow<TokenActionsState> {
operator fun invoke(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
yieldSupplyAvailability: YieldSupplyAvailability = YieldSupplyAvailability.Unavailable,
): Flow<TokenActionsState> {
return when {
cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation -> {
flowOf(value = MissedDerivationsActionsFactory.create())
@ -74,11 +79,12 @@ class GetCryptoCurrencyActionsUseCase(
userWalletId = userWallet.walletId,
currency = cryptoCurrencyStatus.currency,
)
.mapLatest {
.mapLatest { stakingAvailability ->
outdatedDataActionsFactory.create(
userWallet = userWallet,
cryptoCurrencyStatus = cryptoCurrencyStatus,
stakingAvailability = it,
stakingAvailability = stakingAvailability,
yieldSupplyAvailability = yieldSupplyAvailability,
)
}
}
@ -94,6 +100,7 @@ class GetCryptoCurrencyActionsUseCase(
userWallet = userWallet,
cryptoCurrencyStatus = cryptoCurrencyStatus,
stakingAvailability = stakingAvailability,
yieldSupplyAvailability = yieldSupplyAvailability,
shouldShowSwapStories = swapStoryContent != null,
)
}

View file

@ -11,6 +11,7 @@ import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState.ActionState
import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.models.YieldSupplyAvailability
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.withTimeoutOrNull
@ -193,4 +194,13 @@ internal open class BaseActionsFactory(
is AssetRequirementsCondition.RequiredTrustline -> ScenarioUnavailabilityReason.TrustlineRequired
}
}
protected fun ActionAvailabilityBuilder.addYieldSupplyAction(yieldSupplyAvailability: YieldSupplyAvailability) {
if (yieldSupplyAvailability is YieldSupplyAvailability.Available) {
ActionState.YieldMode(
unavailabilityReason = ScenarioUnavailabilityReason.None,
apy = yieldSupplyAvailability.apy,
).addByReason()
}
}
}

View file

@ -9,6 +9,7 @@ import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState.ActionState
import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.models.YieldSupplyAvailability
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
@ -32,12 +33,14 @@ internal class CommonActionsFactory(
* @param userWallet the user's cold wallet
* @param cryptoCurrencyStatus the status of the cryptocurrency
* @param stakingAvailability the staking availability for the cryptocurrency
* @param yieldSupplyAvailability the yield supply availability for the cryptocurrency
* @param shouldShowSwapStories a flag indicating whether to show swap stories
*/
suspend fun create(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
stakingAvailability: StakingAvailability,
yieldSupplyAvailability: YieldSupplyAvailability,
shouldShowSwapStories: Boolean,
): Set<ActionState> = coroutineScope {
val isAddressAvailable = isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)
@ -128,6 +131,10 @@ internal class CommonActionsFactory(
// region HideToken
addHideTokenAction()
// endregion
// region YieldMode
addYieldSupplyAction(yieldSupplyAvailability)
// endregion
}
}

View file

@ -8,6 +8,7 @@ import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState.ActionState
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.models.YieldSupplyAvailability
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
@ -31,11 +32,13 @@ internal class OutdatedDataActionsFactory(
* @param userWallet the user's cold wallet
* @param cryptoCurrencyStatus the status of the cryptocurrency
* @param stakingAvailability the staking availability for the cryptocurrency
* @param yieldSupplyAvailability the yield supply availability for the cryptocurrency
*/
suspend fun create(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
stakingAvailability: StakingAvailability,
yieldSupplyAvailability: YieldSupplyAvailability,
): Set<ActionState> = coroutineScope {
val sources = cryptoCurrencyStatus.value.sources
@ -129,6 +132,10 @@ internal class OutdatedDataActionsFactory(
// region HideToken
addHideTokenAction()
// endregion
// region Yield Mode
addYieldSupplyAction(yieldSupplyAvailability)
// endregion
}
}

View file

@ -34,6 +34,11 @@ data class TokenActionsState(
data class Send(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
data class YieldMode(
override val unavailabilityReason: ScenarioUnavailabilityReason,
val apy: String,
) : ActionState()
data class Analytics(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
data class HideToken(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()

View file

@ -0,0 +1,10 @@
package com.tangem.domain.yield.supply.models
/**
* Represents the availability of yield supply for a given asset.
*/
sealed class YieldSupplyAvailability {
data class Available(val apy: String) : YieldSupplyAvailability()
data object Unavailable : YieldSupplyAvailability()
}

View file

@ -0,0 +1,26 @@
package com.tangem.domain.yield.supply.usecase
import arrow.core.Either
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.yieldSupplyKey
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldSupplyAvailability
class YieldSupplyGetAvailabilityUseCase(
private val yieldSupplyRepository: YieldSupplyRepository,
) {
suspend operator fun invoke(currency: CryptoCurrency): Either<Throwable, YieldSupplyAvailability> = Either.catch {
val tokenCurrency = currency as? CryptoCurrency.Token ?: return@catch YieldSupplyAvailability.Unavailable
val tokens = yieldSupplyRepository.getCachedMarkets().orEmpty()
val cachedStatus = tokens.firstOrNull { it.yieldSupplyKey == tokenCurrency.yieldSupplyKey() }
val yieldSupplyToken = cachedStatus ?: yieldSupplyRepository.getTokenStatus(tokenCurrency)
if (yieldSupplyToken.isActive) {
YieldSupplyAvailability.Available(yieldSupplyToken.apy.toString())
} else {
YieldSupplyAvailability.Unavailable
}
}
}

View file

@ -45,6 +45,8 @@ dependencies {
implementation(projects.domain.settings)
implementation(projects.domain.notifications.models)
implementation(projects.domain.transaction)
implementation(projects.domain.yieldSupply.models)
implementation(projects.domain.yieldSupply)
// FIXME [REDACTED_TASK_KEY]
// Remove the "Buy" and "Sell" actions from the redux middleware.

View file

@ -47,6 +47,7 @@ internal class PortfolioAnalyticsEvent(
TokenActionsBSContentUM.Action.Receive -> "Button - Receive"
TokenActionsBSContentUM.Action.Exchange -> "Button - Swap"
TokenActionsBSContentUM.Action.Stake -> "Button - Stake"
TokenActionsBSContentUM.Action.YieldMode -> "Button - Yield Mode"
else -> "error"
},
params = buildMap {

View file

@ -1,5 +1,6 @@
package com.tangem.features.markets.portfolio.impl.loader
import arrow.core.getOrElse
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
@ -11,6 +12,8 @@ import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.yield.supply.models.YieldSupplyAvailability
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ -30,6 +33,7 @@ internal class PortfolioDataLoader @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
) {
@ -72,7 +76,10 @@ internal class PortfolioDataLoader @Inject constructor(
.flatMapLatest { walletsWithStatuses ->
val actionsFlows = walletsWithStatuses.flatMap { (wallet, statuses) ->
statuses.map { status ->
getCryptoCurrencyActionsUseCase(wallet, status)
val yieldSupplyAvailability = yieldSupplyGetAvailabilityUseCase(status.currency).getOrElse {
YieldSupplyAvailability.Unavailable
}
getCryptoCurrencyActionsUseCase(wallet, status, yieldSupplyAvailability)
.map {
PortfolioData.CryptoCurrencyData(
userWallet = wallet,

View file

@ -71,8 +71,8 @@ internal class PortfolioTokenUMConverter(
): PortfolioTokenUM.QuickActions {
return PortfolioTokenUM.QuickActions(
actions = toQuickActions(cryptoData.actions),
onQuickActionClick = {
when (it) {
onQuickActionClick = { quickActionUM ->
when (quickActionUM) {
QuickActionUM.Buy -> tokenActionsHandler.handle(
action = TokenActionsBSContentUM.Action.Buy,
cryptoCurrencyData = cryptoData,
@ -89,6 +89,10 @@ internal class PortfolioTokenUMConverter(
action = TokenActionsBSContentUM.Action.Stake,
cryptoCurrencyData = cryptoData,
)
is QuickActionUM.YieldMode -> tokenActionsHandler.handle(
action = TokenActionsBSContentUM.Action.YieldMode,
cryptoCurrencyData = cryptoData,
)
}
},
onQuickActionLongClick = {
@ -110,6 +114,7 @@ internal class PortfolioTokenUMConverter(
is TokenActionsState.ActionState.Swap -> QuickActionUM.Exchange(showBadge = action.showBadge)
is TokenActionsState.ActionState.Receive -> QuickActionUM.Receive
is TokenActionsState.ActionState.Stake -> QuickActionUM.Stake
is TokenActionsState.ActionState.YieldMode -> QuickActionUM.YieldMode(apy = action.apy)
else -> null
}?.let(::add)
}

View file

@ -15,11 +15,11 @@ import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
@ -65,6 +65,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
TokenActionsBSContentUM.Action.Sell -> onSellClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Send -> onSendClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Stake -> onStakeClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.YieldMode -> onYieldModeClick(cryptoCurrencyData)
}
}
@ -178,6 +179,32 @@ internal class TokenActionsHandler @AssistedInject constructor(
)
}
private fun onYieldModeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance<TokenActionsState.ActionState.YieldMode>()
.firstOrNull()?.apy ?: return
val (userWalletId, cryptoCurrencyStatus) = cryptoCurrencyData.let { currencyData ->
currencyData.userWallet.walletId to currencyData.status
}
if (cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true) {
router.push(
AppRoute.YieldSupplyActive(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
apy = yieldSupplyApy,
),
)
} else {
router.push(
AppRoute.YieldSupplyPromo(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
apy = yieldSupplyApy,
),
)
}
}
@AssistedFactory
interface Factory {
fun create(

View file

@ -4,6 +4,7 @@ import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.markets.impl.R
@Immutable
@ -39,4 +40,12 @@ internal sealed class QuickActionUM(
description = resourceReference(R.string.stake_token_description),
icon = R.drawable.ic_staking_24,
)
data class YieldMode(
private val apy: String,
) : QuickActionUM(
title = resourceReference(R.string.yield_module_start_earning),
description = resourceReference(R.string.yield_module_main_screen_promo_banner_message, wrappedList(apy)),
icon = R.drawable.ic_analytics_up_mini_24,
)
}

View file

@ -47,6 +47,10 @@ internal data class TokenActionsBSContentUM(
text = resourceReference(R.string.common_stake),
iconRes = R.drawable.ic_staking_24,
),
YieldMode(
text = resourceReference(R.string.yield_module_start_earning),
iconRes = R.drawable.ic_analytics_up_mini_24,
),
;
val order: Int = ordinal

View file

@ -42,6 +42,7 @@ internal class MultiWalletCurrencyActionsConverter(
}
}
@Suppress("LongMethod")
private fun mapTokenActionState(
actionsState: TokenActionsState.ActionState,
cryptoCurrencyStatus: CryptoCurrencyStatus,
@ -105,6 +106,11 @@ internal class MultiWalletCurrencyActionsConverter(
icon = R.drawable.ic_analytics_24
action = { clickIntents.onAnalyticsClick(cryptoCurrencyStatus) }
}
is TokenActionsState.ActionState.YieldMode -> {
title = resourceReference(R.string.yield_module_start_earning)
icon = R.drawable.ic_analytics_up_mini_24
action = { /* no-op */ }
}
}
return TokenActionButtonConfig(