Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-23 17:08:01 +03:00
parent cf49333c22
commit c35da7186d
9 changed files with 99 additions and 131 deletions

View file

@ -21,7 +21,7 @@ object TangemBlogUrlBuilder {
const val RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP = "https://tangem.com/en/blog/post/give-revoke-permission/"
const val YIELD_SUPPLY_HOW_IT_WORKS_URL = "https://tangem.com/en/blog/post/savings-account"
const val YIELD_SUPPLY_HOW_IT_WORKS_URL = "https://tangem.com/en/blog/post/yield-mode"
const val YIELD_SUPPLY_TOS_URL = "https://aave.com/terms-of-service"
const val YIELD_SUPPLY_PRIVACY_URL = "https://aave.com/privacy-policy"
}

View file

@ -16,11 +16,13 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus
import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap
import kotlin.collections.map
internal class DefaultYieldSupplyRepository(
@ -30,6 +32,8 @@ internal class DefaultYieldSupplyRepository(
private val dispatchers: CoroutineDispatcherProvider,
) : YieldSupplyRepository {
private val statusMap: MutableMap<String, YieldSupplyEnterStatus> = ConcurrentHashMap()
override suspend fun getCachedMarkets(): List<YieldMarketToken>? = withContext(dispatchers.io) {
val cache = store.getSyncOrNull().orEmpty()
val domain = cache.map(YieldMarketTokenConverter::convert)
@ -97,6 +101,17 @@ internal class DefaultYieldSupplyRepository(
).getOrThrow().isActive
}
override suspend fun saveTokenProtocolStatus(
cryptoCurrency: CryptoCurrency,
yieldSupplyEnterStatus: YieldSupplyEnterStatus,
) {
statusMap[cryptoCurrency.id.value] = yieldSupplyEnterStatus
}
override fun getTokenProtocolStatus(cryptoCurrencyToken: CryptoCurrency): YieldSupplyEnterStatus? {
return statusMap[cryptoCurrencyToken.id.value]
}
private fun List<YieldMarketToken>.enrichNetworkIds(): List<YieldMarketToken> {
val chainIdMap = Blockchain.entries.associate { it.getChainId() to it.toNetworkId() }
return this.map { token ->

View file

@ -0,0 +1,6 @@
package com.tangem.domain.yield.supply.models
enum class YieldSupplyEnterStatus {
Enter,
Exit,
}

View file

@ -3,6 +3,7 @@ package com.tangem.domain.yield.supply
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus
import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData
import kotlinx.coroutines.flow.Flow
@ -56,4 +57,30 @@ interface YieldSupplyRepository {
*/
@Throws
suspend fun deactivateProtocol(cryptoCurrencyToken: CryptoCurrency.Token): Boolean
/**
* Save the last user-initiated yield protocol action for the given currency.
*
* The saved value helps the UI render an intermediate "processing" state while waiting
* for the definitive protocol status to be fetched from the network. This information
* is transient and not intended to be persisted across app restarts.
*
* @param cryptoCurrencyToken the currency or token the action relates to
* @param yieldSupplyEnterStatus the last action intent: [YieldSupplyEnterStatus.Enter] or [YieldSupplyEnterStatus.Exit]
*/
suspend fun saveTokenProtocolStatus(
cryptoCurrencyToken: CryptoCurrency,
yieldSupplyEnterStatus: YieldSupplyEnterStatus,
)
/**
* Get the last saved user-initiated yield protocol action for the given currency, if any.
*
* Used to determine whether the UI should display an intermediate "processing" state
* until the protocol status retrieved from backend reflects the change.
*
* @param cryptoCurrencyToken the currency or token to query
* @return the last action intent or null if nothing has been recorded
*/
fun getTokenProtocolStatus(cryptoCurrencyToken: CryptoCurrency): YieldSupplyEnterStatus?
}

View file

@ -1,39 +0,0 @@
package com.tangem.features.yield.supply.impl.common
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import javax.inject.Inject
import javax.inject.Singleton
/**
* Trigger for entering/exiting protocol from other components
*/
interface YieldSupplyProtocolTrigger {
suspend fun onEnterProtocol()
suspend fun onExitProtocol()
}
/**
* Listener to observe entering/exiting protocol events
*/
interface YieldSupplyProtocolListener {
val enterProtocolTriggerFlow: Flow<Unit>
val exitProtocolTriggerFlow: Flow<Unit>
}
@Singleton
internal class DefaultYieldSupplyProtocolTrigger @Inject constructor() :
YieldSupplyProtocolTrigger,
YieldSupplyProtocolListener {
override val enterProtocolTriggerFlow = MutableSharedFlow<Unit>()
override val exitProtocolTriggerFlow = MutableSharedFlow<Unit>()
override suspend fun onEnterProtocol() {
enterProtocolTriggerFlow.emit(Unit)
}
override suspend fun onExitProtocol() {
exitProtocolTriggerFlow.emit(Unit)
}
}

View file

@ -3,10 +3,6 @@ package com.tangem.features.yield.supply.impl.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.yield.supply.impl.DefaultYieldSupplyFeatureToggles
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.features.yield.supply.impl.common.DefaultYieldSupplyProtocolTrigger
import com.tangem.features.yield.supply.impl.common.YieldSupplyProtocolListener
import com.tangem.features.yield.supply.impl.common.YieldSupplyProtocolTrigger
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -22,17 +18,4 @@ internal object YieldSupplyFeatureModule {
fun provideYieldFeatureToggles(featureTogglesManager: FeatureTogglesManager): YieldSupplyFeatureToggles {
return DefaultYieldSupplyFeatureToggles(featureTogglesManager)
}
}
@InstallIn(SingletonComponent::class)
@Module
internal interface YieldSupplyProtocolModuleBinds {
@Singleton
@Binds
fun bindYieldSupplyProtocolTrigger(impl: DefaultYieldSupplyProtocolTrigger): YieldSupplyProtocolTrigger
@Singleton
@Binds
fun bindYieldSupplyProtocolListener(impl: DefaultYieldSupplyProtocolTrigger): YieldSupplyProtocolListener
}

View file

@ -16,12 +16,13 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.yieldSupplyNotAllAmountSupplied
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus
import com.tangem.domain.yield.supply.usecase.YieldSupplyActivateUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase
@ -29,7 +30,6 @@ import com.tangem.domain.yield.supply.usecase.YieldSupplyIsAvailableUseCase
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.common.YieldSupplyProtocolListener
import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusSuccessTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -59,7 +59,7 @@ internal class YieldSupplyModel @Inject constructor(
private val yieldSupplyIsAvailableUseCase: YieldSupplyIsAvailableUseCase,
private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase,
private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase,
private val yieldSupplyProtocolListener: YieldSupplyProtocolListener,
private val yieldSupplyRepository: YieldSupplyRepository,
) : Model(), YieldSupplyClickIntents {
private val params = paramsContainer.require<YieldSupplyComponent.Params>()
@ -87,38 +87,6 @@ internal class YieldSupplyModel @Inject constructor(
init {
checkIfYieldSupplyIsAvailable()
observeProtocolEvents()
}
private fun observeProtocolEvents() {
yieldSupplyProtocolListener.exitProtocolTriggerFlow.onEach {
uiState.update {
YieldSupplyUM.Processing.Exit
}
coroutineScope.launch(dispatchers.io) {
delay(PROCESSING_UPDATE_DELAY)
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
),
)
}
}.launchIn(modelScope)
yieldSupplyProtocolListener.enterProtocolTriggerFlow.onEach {
uiState.update {
YieldSupplyUM.Processing.Enter
}
coroutineScope.launch(dispatchers.io) {
delay(PROCESSING_UPDATE_DELAY)
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
),
)
}
}.launchIn(modelScope)
}
private fun checkIfYieldSupplyIsAvailable() {
@ -142,7 +110,6 @@ internal class YieldSupplyModel @Inject constructor(
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = false,
).onEach { maybeCryptoCurrency ->
Timber.tag("getSingleCryptoCurrencyStatusUseCase").d("update $maybeCryptoCurrency")
maybeCryptoCurrency.fold(
ifRight = { cryptoCurrencyStatus ->
cryptoCurrencyStatusFlow.update { cryptoCurrencyStatus }
@ -175,7 +142,7 @@ internal class YieldSupplyModel @Inject constructor(
)
}.onLeft {
Timber.e(it)
uiState.update { YieldSupplyUM.Unavailable }
uiState.update { YieldSupplyUM.Initial }
}
}
}
@ -214,39 +181,46 @@ internal class YieldSupplyModel @Inject constructor(
@Suppress("MaximumLineLength")
private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) {
val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
val hasActiveTransaction = cryptoCurrencyStatus.value.hasCurrentNetworkTransactions
val yieldTransaction = cryptoCurrencyStatus.value.pendingTransactions.firstOrNull {
it.type is TxInfo.TransactionType.YieldSupply
}?.type as? TxInfo.TransactionType.YieldSupply
if ((yieldSupplyStatus == null || yieldSupplyStatus.isActive == false) &&
yieldSupplyRepository.getTokenProtocolStatus(cryptoCurrency) == YieldSupplyEnterStatus.Enter) {
uiState.update {
YieldSupplyUM.Processing.Enter
}
fetchCurrencyWithDelay()
return
}
if (yieldSupplyStatus?.isActive == true &&
yieldSupplyRepository.getTokenProtocolStatus(cryptoCurrency) == YieldSupplyEnterStatus.Exit) {
uiState.update {
YieldSupplyUM.Processing.Exit
}
fetchCurrencyWithDelay()
return
}
sendInfoAboutProtocolStatus(cryptoCurrencyStatus)
when {
hasActiveTransaction && yieldTransaction != null -> {
coroutineScope.launch(dispatchers.io) {
delay(PROCESSING_UPDATE_DELAY)
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
),
)
}
uiState.update {
when (yieldTransaction) {
TxInfo.TransactionType.YieldSupply.Enter -> YieldSupplyUM.Processing.Enter
TxInfo.TransactionType.YieldSupply.Exit -> YieldSupplyUM.Processing.Exit
}
}
}
yieldSupplyStatus?.isActive == true -> {
loadActiveState(
cryptoCurrencyStatus = cryptoCurrencyStatus,
yieldSupplyStatus = yieldSupplyStatus,
)
}
else -> {
loadTokenStatus()
}
if (yieldSupplyStatus?.isActive == true) {
loadActiveState(
cryptoCurrencyStatus = cryptoCurrencyStatus,
yieldSupplyStatus = yieldSupplyStatus,
)
} else {
loadTokenStatus()
}
}
private fun fetchCurrencyWithDelay() {
coroutineScope.launch(dispatchers.io) {
delay(PROCESSING_UPDATE_DELAY)
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
),
)
}
}

View file

@ -20,6 +20,8 @@ import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus
import com.tangem.domain.yield.supply.usecase.YieldSupplyActivateUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyEstimateEnterFeeUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase
@ -28,7 +30,6 @@ import com.tangem.domain.yield.supply.usecase.YieldSupplyStartEarningUseCase
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory
import com.tangem.features.yield.supply.impl.common.YieldSupplyProtocolTrigger
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyFeeUM
import com.tangem.features.yield.supply.impl.common.entity.transformer.YieldSupplyTransactionInProgressTransformer
@ -66,7 +67,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase,
private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase,
private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase,
private val yieldSupplyProtocolTrigger: YieldSupplyProtocolTrigger,
private val yieldSupplyRepository: YieldSupplyRepository,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
private val params: YieldSupplyStartEarningComponent.Params = paramsContainer.require()
@ -252,7 +253,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
)
},
ifRight = {
yieldSupplyProtocolTrigger.onEnterProtocol()
yieldSupplyRepository.saveTokenProtocolStatus(cryptoCurrency, YieldSupplyEnterStatus.Enter)
analytics.send(YieldSupplyAnalytics.FundsEarned)
yieldSupplyActivateUseCase(cryptoCurrency)
modelScope.launch {

View file

@ -16,12 +16,13 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus
import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyStopEarningUseCase
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory
import com.tangem.features.yield.supply.impl.common.YieldSupplyProtocolTrigger
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyFeeUM
import com.tangem.features.yield.supply.impl.common.entity.transformer.YieldSupplyTransactionInProgressTransformer
@ -55,7 +56,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
private val yieldSupplyNotificationsUpdateTrigger: YieldSupplyNotificationsUpdateTrigger,
private val yieldSupplyAlertFactory: YieldSupplyAlertFactory,
private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase,
private val yieldSupplyProtocolTrigger: YieldSupplyProtocolTrigger,
private val yieldSupplyRepository: YieldSupplyRepository,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
private val params: YieldSupplyStopEarningComponent.Params = paramsContainer.require()
@ -158,7 +159,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
)
},
ifRight = {
yieldSupplyProtocolTrigger.onExitProtocol()
yieldSupplyRepository.saveTokenProtocolStatus(cryptoCurrency, YieldSupplyEnterStatus.Exit)
analytics.send(
YieldSupplyAnalytics.FundsWithdrawn(
token = cryptoCurrency.symbol,