Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-29 12:24:07 +04:00
parent eacf096c6f
commit 933003fac6
7 changed files with 112 additions and 63 deletions

View file

@ -204,6 +204,16 @@ internal object YieldSupplyDomainModule {
)
}
@Provides
@Singleton
fun provideYieldSupplyEnterStatusFlowUseCase(
yieldSupplyRepository: YieldSupplyRepository,
): YieldSupplyEnterStatusFlowUseCase {
return YieldSupplyEnterStatusFlowUseCase(
yieldSupplyRepository = yieldSupplyRepository,
)
}
@Provides
@Singleton
fun provideYieldSupplyGetShouldShowMainPromoUseCase(

View file

@ -27,9 +27,10 @@ import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap
internal class DefaultYieldSupplyRepository(
private val yieldSupplyApi: YieldSupplyApi,
@ -40,7 +41,7 @@ internal class DefaultYieldSupplyRepository(
private val appPreferencesStore: AppPreferencesStore,
) : YieldSupplyRepository {
private val statusMap: MutableMap<String, YieldSupplyPendingStatus> = ConcurrentHashMap()
private val statusMapFlow = MutableStateFlow<Map<String, YieldSupplyPendingStatus>>(emptyMap())
override suspend fun getCachedMarkets(): List<YieldMarketToken>? = withContext(dispatchers.io) {
val cache = store.getSyncOrNull().orEmpty()
@ -129,10 +130,12 @@ internal class DefaultYieldSupplyRepository(
yieldSupplyPendingStatus: YieldSupplyPendingStatus?,
) {
val key = getTokenProtocolStatusKey(userWalletId, cryptoCurrency)
if (yieldSupplyPendingStatus != null) {
statusMap[key] = yieldSupplyPendingStatus
} else {
statusMap.remove(key)
statusMapFlow.update { currentMap ->
if (yieldSupplyPendingStatus != null) {
currentMap + (key to yieldSupplyPendingStatus)
} else {
currentMap - key
}
}
}
@ -153,7 +156,15 @@ internal class DefaultYieldSupplyRepository(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): YieldSupplyPendingStatus? {
return statusMap[getTokenProtocolStatusKey(userWalletId, cryptoCurrency)]
return statusMapFlow.value[getTokenProtocolStatusKey(userWalletId, cryptoCurrency)]
}
override fun getTokenProtocolPendingStatusFlow(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Flow<YieldSupplyPendingStatus?> {
val key = getTokenProtocolStatusKey(userWalletId, cryptoCurrency)
return statusMapFlow.map { it[key] }
}
private fun List<YieldMarketToken>.enrichNetworkIds(): List<YieldMarketToken> {

View file

@ -104,6 +104,18 @@ interface YieldSupplyRepository {
cryptoCurrency: CryptoCurrency,
): YieldSupplyPendingStatus?
/**
* Observe the pending status for the given wallet and currency as a [Flow].
*
* @param userWalletId the wallet to observe
* @param cryptoCurrency the currency or token to observe
* @return a [Flow] emitting the current pending status or null if none exists
*/
fun getTokenProtocolPendingStatusFlow(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Flow<YieldSupplyPendingStatus?>
fun getShouldShowYieldPromoBanner(): Flow<Boolean>
suspend fun setShouldShowYieldPromoBanner(shouldShow: Boolean)

View file

@ -0,0 +1,16 @@
package com.tangem.domain.yield.supply.usecase
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
import kotlinx.coroutines.flow.Flow
class YieldSupplyEnterStatusFlowUseCase(
private val yieldSupplyRepository: YieldSupplyRepository,
) {
operator fun invoke(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow<YieldSupplyPendingStatus?> {
return yieldSupplyRepository.getTokenProtocolPendingStatusFlow(userWalletId, cryptoCurrency)
}
}

View file

@ -24,28 +24,24 @@ class YieldSupplyEnterStatusUseCase(
.toSet()
val hasPendingTx = status?.txIds?.any { it in pendingTxHashes } == true
if (hasPendingTx) {
status
val isActive = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true
val isExpired = status != null &&
System.currentTimeMillis() - status.createdAt > STATUS_EXPIRATION_MS
val shouldClearStatus = when {
isActive && status is YieldSupplyPendingStatus.Enter -> true
!isActive && status is YieldSupplyPendingStatus.Exit -> true
isExpired && !hasPendingTx -> true
else -> false
}
if (shouldClearStatus) {
yieldSupplyRepository.saveTokenProtocolPendingStatus(
userWalletId,
cryptoCurrencyStatus.currency,
null,
)
null
} else {
val isActive = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true
val isExpired = status != null &&
System.currentTimeMillis() - status.createdAt > STATUS_EXPIRATION_MS
val shouldClearStatus = when {
isExpired -> true
isActive && status is YieldSupplyPendingStatus.Exit -> false
!isActive && status is YieldSupplyPendingStatus.Enter -> false
else -> true
}
if (shouldClearStatus) {
yieldSupplyRepository.saveTokenProtocolPendingStatus(
userWalletId,
cryptoCurrencyStatus.currency,
null,
)
null
} else {
status
}
status
}
}
}

View file

@ -91,16 +91,13 @@ class YieldSupplyEnterStatusUseCaseTest {
coEvery {
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus.currency)
} returns emptyList()
coEvery {
yieldSupplyRepository.saveTokenProtocolPendingStatus(userWalletId, token, null)
} returns Unit
val result = useCase(userWalletId, cryptoStatus)
assertThat(result.isRight()).isTrue()
val value = (result as Either.Right).value
assertThat(value).isNull()
coVerify { yieldSupplyRepository.saveTokenProtocolPendingStatus(userWalletId, token, null) }
coVerify(exactly = 0) { yieldSupplyRepository.saveTokenProtocolPendingStatus(any(), any(), any()) }
}
@Test
@ -147,7 +144,7 @@ class YieldSupplyEnterStatusUseCaseTest {
@Test
fun `GIVEN exit status with pending tx WHEN invoke THEN returns status`() = runTest {
val token = createToken()
val cryptoStatus = createStatus(token)
val cryptoStatus = createStatus(token, isActive = true)
val pendingTxHash = "0xexit456"
val status = YieldSupplyPendingStatus.Exit(txIds = listOf(pendingTxHash))

View file

@ -31,8 +31,6 @@ import com.tangem.features.yield.supply.impl.R
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
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.transformer.update
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -57,6 +55,7 @@ internal class YieldSupplyModel @Inject constructor(
private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase,
private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase,
private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
private val yieldSupplyEnterStatusFlowUseCase: YieldSupplyEnterStatusFlowUseCase,
private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase,
private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase,
) : Model(), YieldSupplyClickIntents {
@ -71,8 +70,6 @@ internal class YieldSupplyModel @Inject constructor(
var userWallet: UserWallet by Delegates.notNull()
private var latestCryptoCurrencyStatus: CryptoCurrencyStatus? = null
private val loadStatusJobHolder = JobHolder()
private val isFirstCryptoCurrencyStatusEmission = AtomicBoolean(true)
init {
@ -84,7 +81,7 @@ internal class YieldSupplyModel @Inject constructor(
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
val isAvailable = yieldSupplyIsAvailableUseCase(params.userWalletId, params.cryptoCurrency)
if (isAvailable) {
subscribeOnCurrencyStatusUpdates()
loadUserWalletData()
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params(
userWalletId = params.userWalletId,
@ -95,30 +92,12 @@ internal class YieldSupplyModel @Inject constructor(
}
}
private fun subscribeOnCurrencyStatusUpdates() {
private fun loadUserWalletData() {
modelScope.launch {
getUserWalletUseCase(params.userWalletId).fold(
ifRight = { wallet ->
userWallet = wallet
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
userWalletId = params.userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = false,
).onEach { maybeCryptoCurrency ->
maybeCryptoCurrency.fold(
ifRight = { cryptoCurrencyStatus ->
latestCryptoCurrencyStatus = cryptoCurrencyStatus
if (isFirstCryptoCurrencyStatusEmission.compareAndSet(true, false)) {
sendInfoAboutProtocolStatus(cryptoCurrencyStatus)
}
onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus)
},
ifLeft = {
Timber.w(it.toString())
},
)
}.launchIn(modelScope)
subscribeOnCurrencyStatusUpdates()
},
ifLeft = {
Timber.w(it.toString())
@ -128,6 +107,36 @@ internal class YieldSupplyModel @Inject constructor(
}
}
private fun subscribeOnCurrencyStatusUpdates() {
combine(
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
userWalletId = params.userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = false,
),
yieldSupplyEnterStatusFlowUseCase(
userWalletId = params.userWalletId,
cryptoCurrency = cryptoCurrency,
),
) { maybeCryptoCurrency, _ ->
maybeCryptoCurrency
}.flowOn(dispatchers.io)
.onEach { maybeCryptoCurrency ->
maybeCryptoCurrency.fold(
ifRight = { cryptoCurrencyStatus ->
latestCryptoCurrencyStatus = cryptoCurrencyStatus
if (isFirstCryptoCurrencyStatusEmission.compareAndSet(true, false)) {
sendInfoAboutProtocolStatus(cryptoCurrencyStatus)
}
onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus)
},
ifLeft = {
Timber.w(it.toString())
},
)
}.launchIn(modelScope)
}
private suspend fun loadTokenStatus() {
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken)
@ -168,13 +177,11 @@ internal class YieldSupplyModel @Inject constructor(
)
}
private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch(
dispatchers.default,
) {
private suspend fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) {
val isCryptoCurrencyStatusFromCache = cryptoCurrencyStatus.value.sources.networkSource != StatusSource.ACTUAL
val processing = uiState.value is YieldSupplyUM.Processing
if (isCryptoCurrencyStatusFromCache && processing) {
return@launch
return
}
val pendingStatus = yieldSupplyEnterStatusUseCase(
@ -187,7 +194,7 @@ internal class YieldSupplyModel @Inject constructor(
} else {
loadStatus(cryptoCurrencyStatus)
}
}.saveIn(loadStatusJobHolder)
}
private fun showProcessing(status: YieldSupplyPendingStatus) {
uiState.update {