Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-16 17:49:10 +04:00
parent 3b2a867959
commit cf0659d207
23 changed files with 679 additions and 316 deletions

View file

@ -45,8 +45,7 @@ import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Par
import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
import com.tangem.features.yield.supply.api.YieldSupplyActiveComponent import com.tangem.features.yield.supply.api.YieldSupplyEntryComponent
import com.tangem.features.yield.supply.api.YieldSupplyPromoComponent
import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent
import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
@ -118,8 +117,7 @@ internal class ChildFactory @Inject constructor(
private val tangemPayDetailsContainerComponentFactory: TangemPayDetailsContainerComponent.Factory, private val tangemPayDetailsContainerComponentFactory: TangemPayDetailsContainerComponent.Factory,
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory, private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
private val kycComponentFactory: KycComponent.Factory, private val kycComponentFactory: KycComponent.Factory,
private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory, private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
private val yieldSupplyActiveComponentFactory: YieldSupplyActiveComponent.Factory,
private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val feedEntryComponentFactory: FeedEntryComponent.Factory, private val feedEntryComponentFactory: FeedEntryComponent.Factory,
private val feedFeatureToggle: FeedFeatureToggle, private val feedFeatureToggle: FeedFeatureToggle,
@ -706,25 +704,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = kycComponentFactory, componentFactory = kycComponentFactory,
) )
} }
is AppRoute.YieldSupplyPromo -> { is AppRoute.YieldSupplyEntry -> {
createComponentChild( createComponentChild(
context = context, context = context,
params = YieldSupplyPromoComponent.Params( params = YieldSupplyEntryComponent.Params(
userWalletId = route.userWalletId,
currency = route.cryptoCurrency,
apy = route.apy,
),
componentFactory = yieldSupplyPromoComponentFactory,
)
}
is AppRoute.YieldSupplyActive -> {
createComponentChild(
context = context,
params = YieldSupplyActiveComponent.Params(
userWalletId = route.userWalletId, userWalletId = route.userWalletId,
cryptoCurrency = route.cryptoCurrency, cryptoCurrency = route.cryptoCurrency,
apy = route.apy,
), ),
componentFactory = yieldSupplyActiveComponentFactory, componentFactory = yieldSupplyEntryComponentFactory,
) )
} }
is AppRoute.NewsDetails -> { is AppRoute.NewsDetails -> {

View file

@ -456,18 +456,11 @@ sealed class AppRoute(val path: String) : Route {
data class Kyc(val userWalletId: UserWalletId) : AppRoute(path = "/kyc") data class Kyc(val userWalletId: UserWalletId) : AppRoute(path = "/kyc")
@Serializable @Serializable
data class YieldSupplyPromo( data class YieldSupplyEntry(
val userWalletId: UserWalletId, val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency, val cryptoCurrency: CryptoCurrency,
val apy: String, val apy: String,
) : AppRoute(path = "/yield_supply_promo/${userWalletId.stringValue}/${cryptoCurrency.symbol}") ) : AppRoute(path = "/yield_supply_entry/${userWalletId.stringValue}/${cryptoCurrency.symbol}")
@Serializable
data class YieldSupplyActive(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val apy: String,
) : AppRoute(path = "/yield_supply_active/${userWalletId.stringValue}/${cryptoCurrency.symbol}")
@Serializable @Serializable
data class NewsDetails(val newsId: Int) : AppRoute(path = "/news_details/$newsId") data class NewsDetails(val newsId: Int) : AppRoute(path = "/news_details/$newsId")

View file

@ -1,6 +1,7 @@
package com.tangem.data.yield.supply package com.tangem.data.yield.supply
import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.yieldsupply.YieldSupplyProvider import com.tangem.blockchain.yieldsupply.YieldSupplyProvider
import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toBlockchain
@ -19,19 +20,16 @@ import com.tangem.datasource.local.preferences.utils.store
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldMarketToken import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
internal class DefaultYieldSupplyRepository( internal class DefaultYieldSupplyRepository(
@ -43,7 +41,7 @@ internal class DefaultYieldSupplyRepository(
private val appPreferencesStore: AppPreferencesStore, private val appPreferencesStore: AppPreferencesStore,
) : YieldSupplyRepository { ) : YieldSupplyRepository {
private val statusMap: MutableMap<String, YieldSupplyEnterStatus> = ConcurrentHashMap() private val statusMap: MutableMap<String, YieldSupplyPendingStatus> = ConcurrentHashMap()
override suspend fun getCachedMarkets(): List<YieldMarketToken>? = withContext(dispatchers.io) { override suspend fun getCachedMarkets(): List<YieldMarketToken>? = withContext(dispatchers.io) {
val cache = store.getSyncOrNull().orEmpty() val cache = store.getSyncOrNull().orEmpty()
@ -126,23 +124,39 @@ internal class DefaultYieldSupplyRepository(
).getOrThrow().isActive ).getOrThrow().isActive
} }
override suspend fun saveTokenProtocolStatus( override suspend fun saveTokenProtocolPendingStatus(
userWalletId: UserWalletId, userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency, cryptoCurrency: CryptoCurrency,
yieldSupplyEnterStatus: YieldSupplyEnterStatus?, yieldSupplyPendingStatus: YieldSupplyPendingStatus?,
) { ) {
val key = getTokenProtocolStatusKey(userWalletId, cryptoCurrency) val key = getTokenProtocolStatusKey(userWalletId, cryptoCurrency)
if (yieldSupplyEnterStatus != null) { if (yieldSupplyPendingStatus != null) {
statusMap[key] = yieldSupplyEnterStatus statusMap[key] = yieldSupplyPendingStatus
} else { } else {
statusMap.remove(key) statusMap.remove(key)
} }
} }
override fun getTokenProtocolStatus( override suspend fun getPendingTxHashes(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): List<String> {
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
) ?: return emptyList()
return walletManager.wallet.recentTransactions
.filter { it.status == TransactionStatus.Unconfirmed }
.map {
it.hash.orEmpty()
}
}
override fun getTokenProtocolPendingStatus(
userWalletId: UserWalletId, userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency, cryptoCurrency: CryptoCurrency,
): YieldSupplyEnterStatus? { ): YieldSupplyPendingStatus? {
return statusMap[getTokenProtocolStatusKey(userWalletId, cryptoCurrency)] return statusMap[getTokenProtocolStatusKey(userWalletId, cryptoCurrency)]
} }
@ -153,32 +167,6 @@ internal class DefaultYieldSupplyRepository(
} }
} }
override suspend fun getTokenPendingStatus(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): YieldSupplyEnterStatus? = runSuspendCatching {
val cryptoCurrency = cryptoCurrencyStatus.currency
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = cryptoCurrency.network.toBlockchain(),
derivationPath = cryptoCurrency.network.derivationPath.value,
) ?: error("Wallet manager not found")
val pendingTxs = cryptoCurrencyStatus.value.pendingTransactions
val yieldAddress = walletManager.calculateYieldModuleAddress()
val hasRecentYieldEnterTxs = pendingTxs.hasYieldEnterTransactions(yieldAddress)
val hasRecentYieldExitTxs = pendingTxs.hasYieldExitTransactions()
when {
hasRecentYieldEnterTxs -> YieldSupplyEnterStatus.Enter
hasRecentYieldExitTxs -> YieldSupplyEnterStatus.Exit
else -> null
}
}.onFailure { exception ->
Timber.w(exception, "Failed to get pending yield supply status")
}.getOrNull()
override fun getShouldShowYieldPromoBanner(): Flow<Boolean> { override fun getShouldShowYieldPromoBanner(): Flow<Boolean> {
return appPreferencesStore.get(PreferencesKeys.YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY, true) return appPreferencesStore.get(PreferencesKeys.YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY, true)
} }
@ -187,16 +175,6 @@ internal class DefaultYieldSupplyRepository(
appPreferencesStore.store(PreferencesKeys.YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY, shouldShow) appPreferencesStore.store(PreferencesKeys.YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY, shouldShow)
} }
private fun Set<TxInfo>.hasYieldEnterTransactions(yieldAddress: String) = any {
it.type == TxInfo.TransactionType.YieldSupply.Enter ||
it.type == TxInfo.TransactionType.Approve &&
(it.interactionAddressType as? TxInfo.InteractionAddressType.Contract)?.address == yieldAddress
}
private fun Set<TxInfo>.hasYieldExitTransactions() = any {
it.type == TxInfo.TransactionType.YieldSupply.Exit
}
private fun getTokenProtocolStatusKey(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String = private fun getTokenProtocolStatusKey(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String =
"${userWalletId}_${cryptoCurrency.id.value}" "${userWalletId}_${cryptoCurrency.id.value}"
} }

View file

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

View file

@ -0,0 +1,6 @@
package com.tangem.domain.yield.supply.models
sealed class YieldSupplyPendingStatus(open val txIds: List<String>) {
data class Enter(override val txIds: List<String>) : YieldSupplyPendingStatus(txIds)
data class Exit(override val txIds: List<String>) : YieldSupplyPendingStatus(txIds)
}

View file

@ -4,7 +4,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldMarketToken import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -72,14 +72,26 @@ interface YieldSupplyRepository {
* *
* @param userWalletId the wallet the action was performed with * @param userWalletId the wallet the action was performed with
* @param cryptoCurrency the currency or token the action relates to * @param cryptoCurrency the currency or token the action relates to
* @param yieldSupplyEnterStatus the last action intent: [YieldSupplyEnterStatus.Enter] or [YieldSupplyEnterStatus.Exit] * @param yieldSupplyPendingStatus the last action intent: [YieldSupplyPendingStatus.Enter] or [YieldSupplyPendingStatus.Exit]
*/ */
suspend fun saveTokenProtocolStatus( suspend fun saveTokenProtocolPendingStatus(
userWalletId: UserWalletId, userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency, cryptoCurrency: CryptoCurrency,
yieldSupplyEnterStatus: YieldSupplyEnterStatus?, yieldSupplyPendingStatus: YieldSupplyPendingStatus?,
) )
/**
* Retrieve the hashes of pending (unconfirmed) transactions for the given wallet and currency.
*
* @param userWalletId the wallet to query
* @param cryptoCurrencyStatus the currency status containing network information
* @return list of transaction hashes that are still unconfirmed, or an empty list if none exist
*/
suspend fun getPendingTxHashes(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): List<String>
/** /**
* Get the last saved userinitiated yield protocol action for the given wallet and currency, if any. * Get the last saved userinitiated yield protocol action for the given wallet and currency, if any.
* *
@ -91,23 +103,10 @@ interface YieldSupplyRepository {
* @param cryptoCurrency the currency or token to query * @param cryptoCurrency the currency or token to query
* @return the last action intent or null if nothing has been recorded * @return the last action intent or null if nothing has been recorded
*/ */
fun getTokenProtocolStatus(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): YieldSupplyEnterStatus? fun getTokenProtocolPendingStatus(
/**
* Get the pending status of the yield protocol action for the given wallet and 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. The value is
* transient (inmemory only) and is not persisted across app restarts.
*
* @param userWalletId the wallet to query
* @param cryptoCurrencyStatus the currency status to query
* @return the pending action intent or null if nothing has been recorded
*/
suspend fun getTokenPendingStatus(
userWalletId: UserWalletId, userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus, cryptoCurrency: CryptoCurrency,
): YieldSupplyEnterStatus? ): YieldSupplyPendingStatus?
fun getShouldShowYieldPromoBanner(): Flow<Boolean> fun getShouldShowYieldPromoBanner(): Flow<Boolean>

View file

@ -4,21 +4,36 @@ import arrow.core.Either
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
class YieldSupplyEnterStatusUseCase( class YieldSupplyEnterStatusUseCase(
private val yieldSupplyRepository: YieldSupplyRepository, private val yieldSupplyRepository: YieldSupplyRepository,
) { ) {
operator fun invoke( suspend operator fun invoke(
userWalletId: UserWalletId, userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus, cryptoCurrencyStatus: CryptoCurrencyStatus,
): Either<Throwable, YieldSupplyEnterStatus?> { ): Either<Throwable, YieldSupplyPendingStatus?> {
return Either.catch { return Either.catch {
yieldSupplyRepository.getTokenProtocolStatus( val status = yieldSupplyRepository.getTokenProtocolPendingStatus(
userWalletId, userWalletId,
cryptoCurrencyStatus.currency, cryptoCurrencyStatus.currency,
) )
val pendingTxHashes = yieldSupplyRepository
.getPendingTxHashes(userWalletId, cryptoCurrencyStatus)
.toSet()
val hasPendingTx = status?.txIds?.any { it in pendingTxHashes } == true
if (hasPendingTx) {
status
} else {
yieldSupplyRepository.saveTokenProtocolPendingStatus(
userWalletId,
cryptoCurrencyStatus.currency,
null,
)
null
}
} }
} }
} }

View file

@ -0,0 +1,242 @@
package com.tangem.domain.yield.supply.usecase
import arrow.core.Either
import com.google.common.truth.Truth.assertThat
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.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.math.BigDecimal
@OptIn(ExperimentalCoroutinesApi::class)
class YieldSupplyEnterStatusUseCaseTest {
private val yieldSupplyRepository: YieldSupplyRepository = mockk()
private lateinit var useCase: YieldSupplyEnterStatusUseCase
private val userWalletId = UserWalletId("abcdef012345")
@BeforeEach
fun setUp() {
useCase = YieldSupplyEnterStatusUseCase(
yieldSupplyRepository = yieldSupplyRepository,
)
}
@Test
fun `GIVEN pending tx exists in status WHEN invoke THEN returns status`() = runTest {
val token = createToken()
val cryptoStatus = createStatus(token)
val pendingTxHash = "0xabc123"
val status = YieldSupplyPendingStatus.Enter(txIds = listOf(pendingTxHash))
coEvery {
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
} returns status
coEvery {
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus)
} returns listOf(pendingTxHash)
val result = useCase(userWalletId, cryptoStatus)
assertThat(result.isRight()).isTrue()
val value = (result as Either.Right).value
assertThat(value).isEqualTo(status)
}
@Test
fun `GIVEN no pending tx matches status WHEN invoke THEN clears status and returns null`() = runTest {
val token = createToken()
val cryptoStatus = createStatus(token)
val status = YieldSupplyPendingStatus.Enter(txIds = listOf("0xabc123"))
coEvery {
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
} returns status
coEvery {
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus)
} returns listOf("0xdifferent")
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) }
}
@Test
fun `GIVEN status is null WHEN invoke THEN returns null`() = runTest {
val token = createToken()
val cryptoStatus = createStatus(token)
coEvery {
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
} returns null
coEvery {
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus)
} 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()
}
@Test
fun `GIVEN empty pending tx list WHEN invoke THEN clears status and returns null`() = runTest {
val token = createToken()
val cryptoStatus = createStatus(token)
val status = YieldSupplyPendingStatus.Enter(txIds = listOf("0xabc123"))
coEvery {
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
} returns status
coEvery {
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus)
} 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) }
}
@Test
fun `GIVEN repository throws exception WHEN invoke THEN returns left with error`() = runTest {
val token = createToken()
val cryptoStatus = createStatus(token)
val exception = RuntimeException("Network error")
coEvery {
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
} throws exception
val result = useCase(userWalletId, cryptoStatus)
assertThat(result.isLeft()).isTrue()
val error = (result as Either.Left).value
assertThat(error).isEqualTo(exception)
}
@Test
fun `GIVEN exit status with pending tx WHEN invoke THEN returns status`() = runTest {
val token = createToken()
val cryptoStatus = createStatus(token)
val pendingTxHash = "0xexit456"
val status = YieldSupplyPendingStatus.Exit(txIds = listOf(pendingTxHash))
coEvery {
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
} returns status
coEvery {
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus)
} returns listOf(pendingTxHash)
val result = useCase(userWalletId, cryptoStatus)
assertThat(result.isRight()).isTrue()
val value = (result as Either.Right).value
assertThat(value).isEqualTo(status)
}
@Test
fun `GIVEN multiple tx ids with one matching WHEN invoke THEN returns status`() = runTest {
val token = createToken()
val cryptoStatus = createStatus(token)
val matchingTxHash = "0xmatch"
val status = YieldSupplyPendingStatus.Enter(txIds = listOf("0xfirst", matchingTxHash, "0xlast"))
coEvery {
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
} returns status
coEvery {
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus)
} returns listOf(matchingTxHash)
val result = useCase(userWalletId, cryptoStatus)
assertThat(result.isRight()).isTrue()
val value = (result as Either.Right).value
assertThat(value).isEqualTo(status)
}
private fun createToken(): CryptoCurrency.Token {
val rawNetworkId = "ethereum"
val derivationPath = Network.DerivationPath.None
val network = Network(
id = Network.ID(value = rawNetworkId, derivationPath = derivationPath),
backendId = rawNetworkId,
name = rawNetworkId,
currencySymbol = rawNetworkId.take(3).uppercase(),
derivationPath = derivationPath,
isTestnet = false,
standardType = Network.StandardType.Unspecified("UNSPECIFIED"),
hasFiatFeeRate = true,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
)
return CryptoCurrency.Token(
id = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId),
suffix = CryptoCurrency.ID.Suffix.RawID(rawNetworkId),
),
network = network,
name = "TEST_TOKEN",
symbol = "TTK",
decimals = 18,
iconUrl = null,
isCustom = false,
contractAddress = "0xToken",
)
}
private fun createStatus(token: CryptoCurrency.Token): CryptoCurrencyStatus {
return CryptoCurrencyStatus(
currency = token,
value = CryptoCurrencyStatus.Custom(
amount = BigDecimal.ZERO,
fiatAmount = BigDecimal.ZERO,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
stakingBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "0x0000000000000000000000000000000000000000",
type = NetworkAddress.Address.Type.Primary,
),
),
sources = CryptoCurrencyStatus.Sources(),
),
)
}
}

View file

@ -20,8 +20,6 @@ import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
import com.tangem.features.feed.impl.R import com.tangem.features.feed.impl.R
@ -43,7 +41,6 @@ internal class TokenActionsHandler @AssistedInject constructor(
private val isDemoCardUseCase: IsDemoCardUseCase, private val isDemoCardUseCase: IsDemoCardUseCase,
private val messageSender: UiMessageSender, private val messageSender: UiMessageSender,
private val shareManager: ShareManager, private val shareManager: ShareManager,
private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
) { ) {
private val disabledActionsInDemoMode = buildSet { private val disabledActionsInDemoMode = buildSet {
@ -186,41 +183,13 @@ internal class TokenActionsHandler @AssistedInject constructor(
val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance<TokenActionsState.ActionState.YieldMode>() val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance<TokenActionsState.ActionState.YieldMode>()
.firstOrNull()?.apy ?: return .firstOrNull()?.apy ?: return
val (userWalletId, cryptoCurrencyStatus) = cryptoCurrencyData.let { currencyData -> router.push(
currencyData.userWallet.walletId to currencyData.status AppRoute.YieldSupplyEntry(
} userWalletId = cryptoCurrencyData.userWallet.walletId,
val tokenEnterStatus = yieldSupplyEnterStatusUseCase(userWalletId, cryptoCurrencyStatus).getOrNull() cryptoCurrency = cryptoCurrencyData.status.currency,
val isActiveYield = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true apy = yieldSupplyApy,
),
when { )
tokenEnterStatus != null -> router.push(
AppRoute.CurrencyDetails(
userWalletId = userWalletId,
currency = cryptoCurrencyStatus.currency,
navigationAction = NavigationAction.YieldSupply(
isActive = isActiveYield,
),
),
)
isActiveYield -> {
router.push(
AppRoute.YieldSupplyActive(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
apy = yieldSupplyApy,
),
)
}
else -> {
router.push(
AppRoute.YieldSupplyPromo(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
apy = yieldSupplyApy,
),
)
}
}
} }
@AssistedFactory @AssistedFactory

View file

@ -20,8 +20,6 @@ import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase
import com.tangem.features.markets.impl.R import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
@ -43,7 +41,6 @@ internal class TokenActionsHandler @AssistedInject constructor(
private val isDemoCardUseCase: IsDemoCardUseCase, private val isDemoCardUseCase: IsDemoCardUseCase,
private val messageSender: UiMessageSender, private val messageSender: UiMessageSender,
private val shareManager: ShareManager, private val shareManager: ShareManager,
private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
) { ) {
private val disabledActionsInDemoMode = buildSet { private val disabledActionsInDemoMode = buildSet {
@ -186,41 +183,13 @@ internal class TokenActionsHandler @AssistedInject constructor(
val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance<TokenActionsState.ActionState.YieldMode>() val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance<TokenActionsState.ActionState.YieldMode>()
.firstOrNull()?.apy ?: return .firstOrNull()?.apy ?: return
val (userWalletId, cryptoCurrencyStatus) = cryptoCurrencyData.let { currencyData -> router.push(
currencyData.userWallet.walletId to currencyData.status AppRoute.YieldSupplyEntry(
} userWalletId = cryptoCurrencyData.userWallet.walletId,
val tokenEnterStatus = yieldSupplyEnterStatusUseCase(userWalletId, cryptoCurrencyStatus).getOrNull() cryptoCurrency = cryptoCurrencyData.status.currency,
val isActiveYield = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true apy = yieldSupplyApy,
),
when { )
tokenEnterStatus != null -> router.push(
AppRoute.CurrencyDetails(
userWalletId = userWalletId,
currency = cryptoCurrencyStatus.currency,
navigationAction = NavigationAction.YieldSupply(
isActive = isActiveYield,
),
),
)
isActiveYield -> {
router.push(
AppRoute.YieldSupplyActive(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
apy = yieldSupplyApy,
),
)
}
else -> {
router.push(
AppRoute.YieldSupplyPromo(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
apy = yieldSupplyApy,
),
)
}
}
} }
@AssistedFactory @AssistedFactory

View file

@ -22,7 +22,6 @@ import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplySetShouldShowMainPromoUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplySetShouldShowMainPromoUseCase
import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
@ -103,7 +102,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val accountDependencies: AccountDependencies, private val accountDependencies: AccountDependencies,
private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
private val yieldSupplySetShouldShowMainPromoUseCase: YieldSupplySetShouldShowMainPromoUseCase, private val yieldSupplySetShouldShowMainPromoUseCase: YieldSupplySetShouldShowMainPromoUseCase,
private val tokenListAnalyticsSender: TokenListAnalyticsSender, private val tokenListAnalyticsSender: TokenListAnalyticsSender,
) : BaseWalletClickIntents(), WalletContentClickIntents { ) : BaseWalletClickIntents(), WalletContentClickIntents {
@ -199,7 +197,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
is NavigationAction.YieldSupply -> openYieldSupply( is NavigationAction.YieldSupply -> openYieldSupply(
userWalletId = userWalletId, userWalletId = userWalletId,
cryptoCurrencyStatus = currencyStatus, cryptoCurrencyStatus = currencyStatus,
navigationAction = navigationAction,
apy = apy, apy = apy,
) )
} }
@ -242,32 +239,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
accountDependencies.expandedAccountsHolder.collapseAccount(account.accountId) accountDependencies.expandedAccountsHolder.collapseAccount(account.accountId)
} }
private fun openYieldSupply( private fun openYieldSupply(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, apy: String) {
userWalletId: UserWalletId, router.openYieldSupplyEntryScreen(
cryptoCurrencyStatus: CryptoCurrencyStatus, userWalletId = userWalletId,
navigationAction: NavigationAction.YieldSupply, cryptoCurrency = cryptoCurrencyStatus.currency,
apy: String, apy = apy,
) { )
modelScope.launch {
val tokenEnterStatus = yieldSupplyEnterStatusUseCase(userWalletId, cryptoCurrencyStatus).getOrNull()
when {
tokenEnterStatus != null -> router.openTokenDetails(
userWalletId = userWalletId,
currencyStatus = cryptoCurrencyStatus,
navigationAction = navigationAction,
)
navigationAction.isActive -> router.openYieldSupplyActiveScreen(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
apy = apy,
)
else -> router.openYieldSupplyPromoScreen(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
apy = apy,
)
}
}
} }
private fun sendApyLabelClickAnalytics(navigationAction: NavigationAction, currencyStatus: CryptoCurrencyStatus) { private fun sendApyLabelClickAnalytics(navigationAction: NavigationAction, currencyStatus: CryptoCurrencyStatus) {

View file

@ -133,19 +133,9 @@ internal class DefaultWalletRouter @Inject constructor(
) )
} }
override fun openYieldSupplyActiveScreen(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, apy: String) { override fun openYieldSupplyEntryScreen(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, apy: String) {
router.push( router.push(
AppRoute.YieldSupplyActive( AppRoute.YieldSupplyEntry(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
apy = apy,
),
)
}
override fun openYieldSupplyPromoScreen(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, apy: String) {
router.push(
AppRoute.YieldSupplyPromo(
userWalletId = userWalletId, userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
apy = apy, apy = apy,

View file

@ -75,9 +75,6 @@ internal interface InnerWalletRouter {
onWarningAcknowledged: (TokenAction) -> Unit, onWarningAcknowledged: (TokenAction) -> Unit,
) )
/** Open yield supply active screen */ /** Open yield supply entry screen */
fun openYieldSupplyActiveScreen(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, apy: String) fun openYieldSupplyEntryScreen(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, apy: String)
/** Open yield supply promo screen */
fun openYieldSupplyPromoScreen(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, apy: String)
} }

View file

@ -0,0 +1,17 @@
package com.tangem.features.yield.supply.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
interface YieldSupplyEntryComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val apy: String,
)
interface Factory : ComponentFactory<Params, YieldSupplyEntryComponent>
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.yield.supply.api.entry
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.models.currency.CryptoCurrency
/**
* Route for switching yield supply promo and active flows.
*/
sealed class YieldSupplyEntryRoute : Route {
/** Initial empty route with no UI */
data object Empty : YieldSupplyEntryRoute()
/** Route to yield supply promo screen */
data class Promo(
val cryptoCurrency: CryptoCurrency,
val apy: String,
) : YieldSupplyEntryRoute()
/** Route to yield supply active screen */
data class Active(
val cryptoCurrency: CryptoCurrency,
) : YieldSupplyEntryRoute()
}

View file

@ -0,0 +1,124 @@
package com.tangem.features.yield.supply.impl.entry
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.yield.supply.api.YieldSupplyActiveComponent
import com.tangem.features.yield.supply.api.YieldSupplyEntryComponent
import com.tangem.features.yield.supply.api.YieldSupplyPromoComponent
import com.tangem.features.yield.supply.api.entry.YieldSupplyEntryRoute
import com.tangem.features.yield.supply.impl.entry.model.YieldSupplyEntryModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultYieldSupplyEntryComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: YieldSupplyEntryComponent.Params,
private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory,
private val yieldSupplyActiveComponentFactory: YieldSupplyActiveComponent.Factory,
) : YieldSupplyEntryComponent, AppComponentContext by appComponentContext {
private val stackNavigation = StackNavigation<YieldSupplyEntryRoute>()
private val innerRouter = InnerRouter<YieldSupplyEntryRoute>(
stackNavigation = stackNavigation,
popCallback = { onChildBack() },
)
private val model: YieldSupplyEntryModel = getOrCreateModel(
params = params,
router = innerRouter,
)
private val childStack = childStack(
key = "yieldSupplyEntryStack",
source = stackNavigation,
serializer = null,
initialConfiguration = model.initialRoute,
handleBackButton = true,
childFactory = { configuration, factoryContext ->
getChildComponent(
configuration = configuration,
factoryContext = childByContext(
componentContext = factoryContext,
router = innerRouter,
),
)
},
)
@Composable
override fun Content(modifier: Modifier) {
val childStackValue by childStack.subscribeAsState()
Children(
stack = childStackValue,
modifier = modifier,
animation = stackAnimation { fade() },
) { child ->
child.instance.Content(Modifier.fillMaxSize())
}
}
private fun getChildComponent(
configuration: YieldSupplyEntryRoute,
factoryContext: AppComponentContext,
): ComposableContentComponent = when (configuration) {
is YieldSupplyEntryRoute.Empty -> EmptyComponent
is YieldSupplyEntryRoute.Promo -> yieldSupplyPromoComponentFactory.create(
context = factoryContext,
params = YieldSupplyPromoComponent.Params(
userWalletId = params.userWalletId,
currency = configuration.cryptoCurrency,
apy = configuration.apy,
),
)
is YieldSupplyEntryRoute.Active -> yieldSupplyActiveComponentFactory.create(
context = factoryContext,
params = YieldSupplyActiveComponent.Params(
userWalletId = params.userWalletId,
cryptoCurrency = configuration.cryptoCurrency,
),
)
}
private object EmptyComponent : ComposableContentComponent {
@Composable
override fun Content(modifier: Modifier) {
Box(modifier = modifier)
}
}
private fun onChildBack() {
if (childStack.value.backStack.isEmpty() ||
childStack.value.backStack.last().configuration is YieldSupplyEntryRoute.Empty
) {
router.pop()
} else {
stackNavigation.pop()
}
}
@AssistedFactory
interface Factory : YieldSupplyEntryComponent.Factory {
override fun create(
context: AppComponentContext,
params: YieldSupplyEntryComponent.Params,
): DefaultYieldSupplyEntryComponent
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.yield.supply.impl.entry.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.yield.supply.api.YieldSupplyEntryComponent
import com.tangem.features.yield.supply.impl.entry.DefaultYieldSupplyEntryComponent
import com.tangem.features.yield.supply.impl.entry.model.YieldSupplyEntryModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface YieldSupplyEntryBindsModule {
@Binds
@Singleton
fun provideYieldSupplyEntryComponentFactory(
impl: DefaultYieldSupplyEntryComponent.Factory,
): YieldSupplyEntryComponent.Factory
}
@Module
@InstallIn(ModelComponent::class)
internal interface YieldSupplyEntryModelModule {
@Binds
@IntoMap
@ClassKey(YieldSupplyEntryModel::class)
fun provideYieldSupplyEntryModel(impl: YieldSupplyEntryModel): Model
}

View file

@ -0,0 +1,90 @@
package com.tangem.features.yield.supply.impl.entry.model
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase
import com.tangem.features.yield.supply.api.YieldSupplyEntryComponent
import com.tangem.features.yield.supply.api.entry.YieldSupplyEntryRoute
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ModelScoped
internal class YieldSupplyEntryModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
) : Model() {
private val params = paramsContainer.require<YieldSupplyEntryComponent.Params>()
val initialRoute: YieldSupplyEntryRoute = YieldSupplyEntryRoute.Empty
init {
navigateToInitialRoute()
}
private fun navigateToInitialRoute() {
val userWalletId = params.userWalletId
val cryptoCurrency = params.cryptoCurrency
modelScope.launch(dispatchers.default) {
getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrency.id,
).onLeft { error ->
Timber.e("Failed to get CryptoCurrencyStatus: $error")
router.pop()
}.onRight { cryptoCurrencyStatus ->
val route = getInitialRoute(cryptoCurrencyStatus)
if (route != null) {
router.replaceCurrent(route)
}
}
}
}
private suspend fun getInitialRoute(cryptoCurrencyStatus: CryptoCurrencyStatus): YieldSupplyEntryRoute? {
val userWalletId = params.userWalletId
val cryptoCurrency = params.cryptoCurrency
val token = cryptoCurrency as? CryptoCurrency.Token
if (token == null) {
router.pop()
return null
}
val tokenEnterStatus = yieldSupplyEnterStatusUseCase(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
).getOrNull()
val isActiveYield = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true
if (tokenEnterStatus != null) {
router.replaceCurrent(
AppRoute.CurrencyDetails(
userWalletId = userWalletId,
currency = token,
navigationAction = NavigationAction.YieldSupply(
isActive = isActiveYield,
),
),
)
return null
}
return if (isActiveYield) {
YieldSupplyEntryRoute.Active(cryptoCurrency = token)
} else {
YieldSupplyEntryRoute.Promo(cryptoCurrency = token, apy = params.apy)
}
}
}

View file

@ -2,8 +2,6 @@ package com.tangem.features.yield.supply.impl.main.model
import arrow.core.getOrElse import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import android.os.SystemClock
import com.tangem.common.routing.AppRoute.YieldSupplyPromo
import com.tangem.common.routing.AppRouter import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
@ -25,8 +23,7 @@ import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus
import com.tangem.domain.yield.supply.usecase.* import com.tangem.domain.yield.supply.usecase.*
import com.tangem.features.yield.supply.api.YieldSupplyComponent import com.tangem.features.yield.supply.api.YieldSupplyComponent
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
@ -63,7 +60,7 @@ internal class YieldSupplyModel @Inject constructor(
private val yieldSupplyIsAvailableUseCase: YieldSupplyIsAvailableUseCase, private val yieldSupplyIsAvailableUseCase: YieldSupplyIsAvailableUseCase,
private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase, private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase,
private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase, private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase,
private val yieldSupplyRepository: YieldSupplyRepository, private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase, private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase,
private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase, private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase,
) : Model(), YieldSupplyClickIntents { ) : Model(), YieldSupplyClickIntents {
@ -76,11 +73,11 @@ internal class YieldSupplyModel @Inject constructor(
private val cryptoCurrency = params.cryptoCurrency private val cryptoCurrency = params.cryptoCurrency
private var appCurrency: AppCurrency = AppCurrency.Default private var appCurrency: AppCurrency = AppCurrency.Default
var userWallet: UserWallet by Delegates.notNull() var userWallet: UserWallet by Delegates.notNull()
private var latestCryptoCurrencyStatus: CryptoCurrencyStatus? = null
private val fetchCurrencyJobHolder = JobHolder() private val fetchCurrencyJobHolder = JobHolder()
private val loadStatusJobHolder = JobHolder() private val loadStatusJobHolder = JobHolder()
private var lastStatusCheckTimestamp = 0L
private val isFirstCryptoCurrencyStatusEmission = AtomicBoolean(true) private val isFirstCryptoCurrencyStatusEmission = AtomicBoolean(true)
init { init {
@ -116,6 +113,7 @@ internal class YieldSupplyModel @Inject constructor(
).onEach { maybeCryptoCurrency -> ).onEach { maybeCryptoCurrency ->
maybeCryptoCurrency.fold( maybeCryptoCurrency.fold(
ifRight = { cryptoCurrencyStatus -> ifRight = { cryptoCurrencyStatus ->
latestCryptoCurrencyStatus = cryptoCurrencyStatus
if (isFirstCryptoCurrencyStatusEmission.compareAndSet(true, false)) { if (isFirstCryptoCurrencyStatusEmission.compareAndSet(true, false)) {
sendInfoAboutProtocolStatus(cryptoCurrencyStatus) sendInfoAboutProtocolStatus(cryptoCurrencyStatus)
} }
@ -152,96 +150,55 @@ internal class YieldSupplyModel @Inject constructor(
} }
override fun onStartEarningClick() { override fun onStartEarningClick() {
val apy = when (val yieldSupplyUM = uiState.value) { navigateToYieldSupplyEntry()
is YieldSupplyUM.Available -> yieldSupplyUM.apy
is YieldSupplyUM.Content -> yieldSupplyUM.apy
else -> ""
}
appRouter.push(
YieldSupplyPromo(
userWalletId = params.userWalletId,
cryptoCurrency = params.cryptoCurrency,
apy = apy,
),
)
} }
override fun onActiveClick() { override fun onActiveClick() {
navigateToYieldSupplyEntry()
}
private fun navigateToYieldSupplyEntry() {
val cryptoCurrencyStatus = latestCryptoCurrencyStatus ?: return
val apy = when (val yieldSupplyUM = uiState.value) { val apy = when (val yieldSupplyUM = uiState.value) {
is YieldSupplyUM.Available -> yieldSupplyUM.apy is YieldSupplyUM.Available -> yieldSupplyUM.apy
is YieldSupplyUM.Content -> yieldSupplyUM.apy is YieldSupplyUM.Content -> yieldSupplyUM.apy
else -> "" else -> ""
} }
appRouter.push( appRouter.push(
AppRoute.YieldSupplyActive( AppRoute.YieldSupplyEntry(
userWalletId = params.userWalletId, userWalletId = params.userWalletId,
cryptoCurrency = params.cryptoCurrency, cryptoCurrency = cryptoCurrencyStatus.currency,
apy = apy, apy = apy,
), ),
) )
} }
@Suppress("MaximumLineLength")
private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch( private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch(
dispatchers.default, dispatchers.default,
) { ) {
val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
val tokenProtocolStatus = yieldSupplyRepository.getTokenProtocolStatus(
userWallet.walletId,
cryptoCurrency,
)
val tokenPendingStatus = yieldSupplyRepository.getTokenPendingStatus(
userWallet.walletId,
cryptoCurrencyStatus,
)
val isActive = yieldSupplyStatus?.isActive == true
val isCryptoCurrencyStatusFromCache = cryptoCurrencyStatus.value.sources.networkSource != StatusSource.ACTUAL val isCryptoCurrencyStatusFromCache = cryptoCurrencyStatus.value.sources.networkSource != StatusSource.ACTUAL
val processing = uiState.value is YieldSupplyUM.Processing val processing = uiState.value is YieldSupplyUM.Processing
Timber.d(
"YIELD " +
"yieldSupplyStatus $yieldSupplyStatus " +
"tokenProtocolStatus $tokenProtocolStatus " +
"tokenPendingStatus $tokenPendingStatus " +
"isActive $isActive " +
"processing $processing " +
"isCryptoCurrencyStatusFromCache $isCryptoCurrencyStatusFromCache",
)
if (isCryptoCurrencyStatusFromCache && processing) { if (isCryptoCurrencyStatusFromCache && processing) {
return@launch return@launch
} }
when { val pendingStatus = yieldSupplyEnterStatusUseCase(
tokenProtocolStatus != null && tokenPendingStatus != null -> { userWalletId = userWallet.walletId,
showProcessing(tokenPendingStatus) cryptoCurrencyStatus = cryptoCurrencyStatus,
lastStatusCheckTimestamp = 0L ).getOrNull()
}
tokenProtocolStatus == YieldSupplyEnterStatus.Exit && isActive || if (pendingStatus != null) {
tokenProtocolStatus == YieldSupplyEnterStatus.Enter && !isActive -> { showProcessing(pendingStatus)
if (lastStatusCheckTimestamp != 0L) { } else {
if (SystemClock.elapsedRealtime() - lastStatusCheckTimestamp > MAX_STATUS_CHECK_LIMIT) { loadStatus(cryptoCurrencyStatus)
loadStatus(cryptoCurrencyStatus)
lastStatusCheckTimestamp = 0L
} else {
showProcessing(tokenProtocolStatus)
}
} else {
showProcessing(tokenProtocolStatus)
lastStatusCheckTimestamp = SystemClock.elapsedRealtime()
}
}
else -> {
loadStatus(cryptoCurrencyStatus)
lastStatusCheckTimestamp = 0L
}
} }
}.saveIn(loadStatusJobHolder) }.saveIn(loadStatusJobHolder)
private fun showProcessing(status: YieldSupplyEnterStatus) { private fun showProcessing(status: YieldSupplyPendingStatus) {
uiState.update { uiState.update {
when (status) { when (status) {
YieldSupplyEnterStatus.Enter -> YieldSupplyUM.Processing.Enter is YieldSupplyPendingStatus.Enter -> YieldSupplyUM.Processing.Enter
YieldSupplyEnterStatus.Exit -> YieldSupplyUM.Processing.Exit is YieldSupplyPendingStatus.Exit -> YieldSupplyUM.Processing.Exit
} }
} }
fetchCurrencyWithDelay() fetchCurrencyWithDelay()
@ -249,11 +206,6 @@ internal class YieldSupplyModel @Inject constructor(
private suspend fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) { private suspend fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) {
val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
yieldSupplyRepository.saveTokenProtocolStatus(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,
yieldSupplyEnterStatus = null,
)
if (yieldSupplyStatus?.isActive == true) { if (yieldSupplyStatus?.isActive == true) {
loadActiveState( loadActiveState(
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
@ -385,6 +337,5 @@ internal class YieldSupplyModel @Inject constructor(
private companion object { private companion object {
const val PROCESSING_UPDATE_DELAY = 10_000L const val PROCESSING_UPDATE_DELAY = 10_000L
const val MAX_STATUS_CHECK_LIMIT = 10_000L
} }
} }

View file

@ -21,7 +21,7 @@ import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
import com.tangem.domain.yield.supply.usecase.* import com.tangem.domain.yield.supply.usecase.*
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.R
@ -245,18 +245,21 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
}, },
) )
}, },
ifRight = { ifRight = { txsData ->
onStartEarningTransactionSuccess(yieldSupplyFeeUM) onStartEarningTransactionSuccess(yieldSupplyFeeUM, txsData)
}, },
) )
} }
} }
private suspend fun onStartEarningTransactionSuccess(yieldSupplyFeeUM: YieldSupplyFeeUM.Content) { private suspend fun onStartEarningTransactionSuccess(
yieldSupplyRepository.saveTokenProtocolStatus( yieldSupplyFeeUM: YieldSupplyFeeUM.Content,
userWalletId, txsData: List<String>,
cryptoCurrency, ) {
YieldSupplyEnterStatus.Enter, yieldSupplyRepository.saveTokenProtocolPendingStatus(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
yieldSupplyPendingStatus = YieldSupplyPendingStatus.Enter(txsData),
) )
val event = AnalyticsParam.TxSentFrom.Earning( val event = AnalyticsParam.TxSentFrom.Earning(
blockchain = cryptoCurrency.network.name, blockchain = cryptoCurrency.network.name,

View file

@ -21,7 +21,7 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.yield.supply.INCREASE_GAS_LIMIT_FOR_SUPPLY import com.tangem.domain.yield.supply.INCREASE_GAS_LIMIT_FOR_SUPPLY
import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.increaseGasLimitBy import com.tangem.domain.yield.supply.increaseGasLimitBy
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyStopEarningUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyStopEarningUseCase
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
@ -167,18 +167,18 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
) )
params.callback.onTransactionProgress(false) params.callback.onTransactionProgress(false)
}, },
ifRight = { ifRight = { txData ->
onStopEarningTransactionSuccess() onStopEarningTransactionSuccess(txData)
}, },
) )
} }
} }
private suspend fun onStopEarningTransactionSuccess() { private suspend fun onStopEarningTransactionSuccess(txId: String) {
yieldSupplyRepository.saveTokenProtocolStatus( yieldSupplyRepository.saveTokenProtocolPendingStatus(
userWallet.walletId, userWalletId = userWallet.walletId,
cryptoCurrency, cryptoCurrency = cryptoCurrency,
YieldSupplyEnterStatus.Exit, yieldSupplyPendingStatus = YieldSupplyPendingStatus.Exit(listOf(txId)),
) )
analytics.send( analytics.send(
YieldSupplyAnalytics.FundsWithdrawn( YieldSupplyAnalytics.FundsWithdrawn(

View file

@ -5,7 +5,7 @@
# https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/tangem-sdk-android/
# https://github.com/tangem/vico # https://github.com/tangem/vico
tangemBlockchainSdk = "develop-1365" tangemBlockchainSdk = "develop-1366"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-573" tangemCardSdk = "develop-573"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^

View file

@ -36,6 +36,9 @@ internal class WalletManagerFactoryCreator @Inject constructor(
accountCreator = accountCreator, accountCreator = accountCreator,
featureToggles = BlockchainFeatureToggles( featureToggles = BlockchainFeatureToggles(
isYieldSupplyEnabled = featureTogglesManager.isFeatureEnabled("YIELD_SUPPLY_FEATURE_ENABLED"), isYieldSupplyEnabled = featureTogglesManager.isFeatureEnabled("YIELD_SUPPLY_FEATURE_ENABLED"),
isPendingTransactionsEnabled = featureTogglesManager.isFeatureEnabled(
"YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED",
),
), ),
blockchainDataStorage = blockchainDataStorage, blockchainDataStorage = blockchainDataStorage,
loggers = listOf(blockchainSDKLogger), loggers = listOf(blockchainSDKLogger),