diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 83d4c9f04b..f45812a062 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -45,8 +45,7 @@ import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Par import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent -import com.tangem.features.yield.supply.api.YieldSupplyActiveComponent -import com.tangem.features.yield.supply.api.YieldSupplyPromoComponent +import com.tangem.features.yield.supply.api.YieldSupplyEntryComponent 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.cardsettings.api.CardSettingsComponent @@ -118,8 +117,7 @@ internal class ChildFactory @Inject constructor( private val tangemPayDetailsContainerComponentFactory: TangemPayDetailsContainerComponent.Factory, private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory, private val kycComponentFactory: KycComponent.Factory, - private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory, - private val yieldSupplyActiveComponentFactory: YieldSupplyActiveComponent.Factory, + private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val feedEntryComponentFactory: FeedEntryComponent.Factory, private val feedFeatureToggle: FeedFeatureToggle, @@ -706,25 +704,15 @@ internal class ChildFactory @Inject constructor( componentFactory = kycComponentFactory, ) } - is AppRoute.YieldSupplyPromo -> { + is AppRoute.YieldSupplyEntry -> { createComponentChild( context = context, - params = YieldSupplyPromoComponent.Params( - userWalletId = route.userWalletId, - currency = route.cryptoCurrency, - apy = route.apy, - ), - componentFactory = yieldSupplyPromoComponentFactory, - ) - } - is AppRoute.YieldSupplyActive -> { - createComponentChild( - context = context, - params = YieldSupplyActiveComponent.Params( + params = YieldSupplyEntryComponent.Params( userWalletId = route.userWalletId, cryptoCurrency = route.cryptoCurrency, + apy = route.apy, ), - componentFactory = yieldSupplyActiveComponentFactory, + componentFactory = yieldSupplyEntryComponentFactory, ) } is AppRoute.NewsDetails -> { diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index ae5ccdd4d1..f70bb78a58 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -456,18 +456,11 @@ sealed class AppRoute(val path: String) : Route { data class Kyc(val userWalletId: UserWalletId) : AppRoute(path = "/kyc") @Serializable - data class YieldSupplyPromo( + data class YieldSupplyEntry( val userWalletId: UserWalletId, val cryptoCurrency: CryptoCurrency, val apy: String, - ) : AppRoute(path = "/yield_supply_promo/${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}") + ) : AppRoute(path = "/yield_supply_entry/${userWalletId.stringValue}/${cryptoCurrency.symbol}") @Serializable data class NewsDetails(val newsId: Int) : AppRoute(path = "/news_details/$newsId") diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt index d7faa6176a..a2f8f3700f 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.yield.supply import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionStatus import com.tangem.blockchain.yieldsupply.YieldSupplyProvider import com.tangem.blockchainsdk.utils.fromNetworkId 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.domain.models.currency.CryptoCurrency 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.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.YieldSupplyPendingStatus import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap internal class DefaultYieldSupplyRepository( @@ -43,7 +41,7 @@ internal class DefaultYieldSupplyRepository( private val appPreferencesStore: AppPreferencesStore, ) : YieldSupplyRepository { - private val statusMap: MutableMap = ConcurrentHashMap() + private val statusMap: MutableMap = ConcurrentHashMap() override suspend fun getCachedMarkets(): List? = withContext(dispatchers.io) { val cache = store.getSyncOrNull().orEmpty() @@ -126,23 +124,39 @@ internal class DefaultYieldSupplyRepository( ).getOrThrow().isActive } - override suspend fun saveTokenProtocolStatus( + override suspend fun saveTokenProtocolPendingStatus( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - yieldSupplyEnterStatus: YieldSupplyEnterStatus?, + yieldSupplyPendingStatus: YieldSupplyPendingStatus?, ) { val key = getTokenProtocolStatusKey(userWalletId, cryptoCurrency) - if (yieldSupplyEnterStatus != null) { - statusMap[key] = yieldSupplyEnterStatus + if (yieldSupplyPendingStatus != null) { + statusMap[key] = yieldSupplyPendingStatus } else { statusMap.remove(key) } } - override fun getTokenProtocolStatus( + override suspend fun getPendingTxHashes( + userWalletId: UserWalletId, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): List { + 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, cryptoCurrency: CryptoCurrency, - ): YieldSupplyEnterStatus? { + ): YieldSupplyPendingStatus? { 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 { 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) } - private fun Set.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.hasYieldExitTransactions() = any { - it.type == TxInfo.TransactionType.YieldSupply.Exit - } - private fun getTokenProtocolStatusKey(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String = "${userWalletId}_${cryptoCurrency.id.value}" } \ No newline at end of file diff --git a/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyEnterStatus.kt b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyEnterStatus.kt deleted file mode 100644 index 4ecdb12732..0000000000 --- a/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyEnterStatus.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.domain.yield.supply.models - -enum class YieldSupplyEnterStatus { - Enter, - Exit, -} \ No newline at end of file diff --git a/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyPendingStatus.kt b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyPendingStatus.kt new file mode 100644 index 0000000000..ec24a8c5d7 --- /dev/null +++ b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyPendingStatus.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.yield.supply.models + +sealed class YieldSupplyPendingStatus(open val txIds: List) { + data class Enter(override val txIds: List) : YieldSupplyPendingStatus(txIds) + data class Exit(override val txIds: List) : YieldSupplyPendingStatus(txIds) +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt index 9a73322ca8..7e371b0a14 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt @@ -4,7 +4,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus 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.YieldSupplyPendingStatus import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData import kotlinx.coroutines.flow.Flow @@ -72,14 +72,26 @@ interface YieldSupplyRepository { * * @param userWalletId the wallet the action was performed with * @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, 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 + /** * Get the last saved user‑initiated 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 * @return the last action intent or null if nothing has been recorded */ - fun getTokenProtocolStatus(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): YieldSupplyEnterStatus? - - /** - * 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 (in‑memory 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( + fun getTokenProtocolPendingStatus( userWalletId: UserWalletId, - cryptoCurrencyStatus: CryptoCurrencyStatus, - ): YieldSupplyEnterStatus? + cryptoCurrency: CryptoCurrency, + ): YieldSupplyPendingStatus? fun getShouldShowYieldPromoBanner(): Flow diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCase.kt index 35de1341bb..726f1cd3df 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCase.kt @@ -4,21 +4,36 @@ import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId 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( private val yieldSupplyRepository: YieldSupplyRepository, ) { - operator fun invoke( + suspend operator fun invoke( userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, - ): Either { + ): Either { return Either.catch { - yieldSupplyRepository.getTokenProtocolStatus( + val status = yieldSupplyRepository.getTokenProtocolPendingStatus( userWalletId, 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 + } } } } \ No newline at end of file diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCaseTest.kt new file mode 100644 index 0000000000..54955b5312 --- /dev/null +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCaseTest.kt @@ -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(), + ), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt index 0762d2602e..55d52cea7d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt @@ -20,8 +20,6 @@ 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.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.ui.state.TokenActionsBSContentUM import com.tangem.features.feed.impl.R @@ -43,7 +41,6 @@ internal class TokenActionsHandler @AssistedInject constructor( private val isDemoCardUseCase: IsDemoCardUseCase, private val messageSender: UiMessageSender, private val shareManager: ShareManager, - private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase, ) { private val disabledActionsInDemoMode = buildSet { @@ -186,41 +183,13 @@ internal class TokenActionsHandler @AssistedInject constructor( val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance() .firstOrNull()?.apy ?: return - val (userWalletId, cryptoCurrencyStatus) = cryptoCurrencyData.let { currencyData -> - currencyData.userWallet.walletId to currencyData.status - } - val tokenEnterStatus = yieldSupplyEnterStatusUseCase(userWalletId, cryptoCurrencyStatus).getOrNull() - val isActiveYield = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true - - 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, - ), - ) - } - } + router.push( + AppRoute.YieldSupplyEntry( + userWalletId = cryptoCurrencyData.userWallet.walletId, + cryptoCurrency = cryptoCurrencyData.status.currency, + apy = yieldSupplyApy, + ), + ) } @AssistedFactory diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index 7e6523b992..853080d794 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -20,8 +20,6 @@ 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.tokens.model.details.NavigationAction -import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase 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 @@ -43,7 +41,6 @@ internal class TokenActionsHandler @AssistedInject constructor( private val isDemoCardUseCase: IsDemoCardUseCase, private val messageSender: UiMessageSender, private val shareManager: ShareManager, - private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase, ) { private val disabledActionsInDemoMode = buildSet { @@ -186,41 +183,13 @@ internal class TokenActionsHandler @AssistedInject constructor( val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance() .firstOrNull()?.apy ?: return - val (userWalletId, cryptoCurrencyStatus) = cryptoCurrencyData.let { currencyData -> - currencyData.userWallet.walletId to currencyData.status - } - val tokenEnterStatus = yieldSupplyEnterStatusUseCase(userWalletId, cryptoCurrencyStatus).getOrNull() - val isActiveYield = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true - - 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, - ), - ) - } - } + router.push( + AppRoute.YieldSupplyEntry( + userWalletId = cryptoCurrencyData.userWallet.walletId, + cryptoCurrency = cryptoCurrencyData.status.currency, + apy = yieldSupplyApy, + ), + ) } @AssistedFactory diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index 5b3b279808..b3f825d821 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -22,7 +22,6 @@ import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase 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.feature.wallet.presentation.account.AccountDependencies 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 hotWalletFeatureToggles: HotWalletFeatureToggles, private val accountDependencies: AccountDependencies, - private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase, private val yieldSupplySetShouldShowMainPromoUseCase: YieldSupplySetShouldShowMainPromoUseCase, private val tokenListAnalyticsSender: TokenListAnalyticsSender, ) : BaseWalletClickIntents(), WalletContentClickIntents { @@ -199,7 +197,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( is NavigationAction.YieldSupply -> openYieldSupply( userWalletId = userWalletId, cryptoCurrencyStatus = currencyStatus, - navigationAction = navigationAction, apy = apy, ) } @@ -242,32 +239,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( accountDependencies.expandedAccountsHolder.collapseAccount(account.accountId) } - private fun openYieldSupply( - userWalletId: UserWalletId, - cryptoCurrencyStatus: CryptoCurrencyStatus, - navigationAction: NavigationAction.YieldSupply, - apy: String, - ) { - 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 openYieldSupply(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, apy: String) { + router.openYieldSupplyEntryScreen( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + apy = apy, + ) } private fun sendApyLabelClickAnalytics(navigationAction: NavigationAction, currencyStatus: CryptoCurrencyStatus) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 934d258f18..49f35c817d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -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( - AppRoute.YieldSupplyActive( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrency, - apy = apy, - ), - ) - } - - override fun openYieldSupplyPromoScreen(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, apy: String) { - router.push( - AppRoute.YieldSupplyPromo( + AppRoute.YieldSupplyEntry( userWalletId = userWalletId, cryptoCurrency = cryptoCurrency, apy = apy, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index c423b80481..afad2d5fe7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -75,9 +75,6 @@ internal interface InnerWalletRouter { onWarningAcknowledged: (TokenAction) -> Unit, ) - /** Open yield supply active screen */ - fun openYieldSupplyActiveScreen(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, apy: String) - - /** Open yield supply promo screen */ - fun openYieldSupplyPromoScreen(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, apy: String) + /** Open yield supply entry screen */ + fun openYieldSupplyEntryScreen(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, apy: String) } \ No newline at end of file diff --git a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyEntryComponent.kt b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyEntryComponent.kt new file mode 100644 index 0000000000..150e34aa92 --- /dev/null +++ b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyEntryComponent.kt @@ -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 +} \ No newline at end of file diff --git a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/entry/YieldSupplyEntryRoute.kt b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/entry/YieldSupplyEntryRoute.kt new file mode 100644 index 0000000000..030660a4a2 --- /dev/null +++ b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/entry/YieldSupplyEntryRoute.kt @@ -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() +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/DefaultYieldSupplyEntryComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/DefaultYieldSupplyEntryComponent.kt new file mode 100644 index 0000000000..0d5f50a0c7 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/DefaultYieldSupplyEntryComponent.kt @@ -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() + + private val innerRouter = InnerRouter( + 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 + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/di/YieldSupplyEntryModule.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/di/YieldSupplyEntryModule.kt new file mode 100644 index 0000000000..0b8df9a27f --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/di/YieldSupplyEntryModule.kt @@ -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 +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/model/YieldSupplyEntryModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/model/YieldSupplyEntryModel.kt new file mode 100644 index 0000000000..2bd65b5bf0 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/model/YieldSupplyEntryModel.kt @@ -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() + + 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) + } + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index 17fba6e4c0..bbca4eb5ec 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -2,8 +2,6 @@ package com.tangem.features.yield.supply.impl.main.model import arrow.core.getOrElse 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.core.analytics.api.AnalyticsEventHandler 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.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.models.YieldSupplyPendingStatus import com.tangem.domain.yield.supply.usecase.* import com.tangem.features.yield.supply.api.YieldSupplyComponent import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics @@ -63,7 +60,7 @@ internal class YieldSupplyModel @Inject constructor( private val yieldSupplyIsAvailableUseCase: YieldSupplyIsAvailableUseCase, private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase, private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase, - private val yieldSupplyRepository: YieldSupplyRepository, + private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase, private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase, private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase, ) : Model(), YieldSupplyClickIntents { @@ -76,11 +73,11 @@ internal class YieldSupplyModel @Inject constructor( private val cryptoCurrency = params.cryptoCurrency private var appCurrency: AppCurrency = AppCurrency.Default var userWallet: UserWallet by Delegates.notNull() + private var latestCryptoCurrencyStatus: CryptoCurrencyStatus? = null private val fetchCurrencyJobHolder = JobHolder() private val loadStatusJobHolder = JobHolder() - private var lastStatusCheckTimestamp = 0L private val isFirstCryptoCurrencyStatusEmission = AtomicBoolean(true) init { @@ -116,6 +113,7 @@ internal class YieldSupplyModel @Inject constructor( ).onEach { maybeCryptoCurrency -> maybeCryptoCurrency.fold( ifRight = { cryptoCurrencyStatus -> + latestCryptoCurrencyStatus = cryptoCurrencyStatus if (isFirstCryptoCurrencyStatusEmission.compareAndSet(true, false)) { sendInfoAboutProtocolStatus(cryptoCurrencyStatus) } @@ -152,96 +150,55 @@ internal class YieldSupplyModel @Inject constructor( } override fun onStartEarningClick() { - val apy = when (val yieldSupplyUM = uiState.value) { - is YieldSupplyUM.Available -> yieldSupplyUM.apy - is YieldSupplyUM.Content -> yieldSupplyUM.apy - else -> "" - } - appRouter.push( - YieldSupplyPromo( - userWalletId = params.userWalletId, - cryptoCurrency = params.cryptoCurrency, - apy = apy, - ), - ) + navigateToYieldSupplyEntry() } override fun onActiveClick() { + navigateToYieldSupplyEntry() + } + + private fun navigateToYieldSupplyEntry() { + val cryptoCurrencyStatus = latestCryptoCurrencyStatus ?: return val apy = when (val yieldSupplyUM = uiState.value) { is YieldSupplyUM.Available -> yieldSupplyUM.apy is YieldSupplyUM.Content -> yieldSupplyUM.apy else -> "" } appRouter.push( - AppRoute.YieldSupplyActive( + AppRoute.YieldSupplyEntry( userWalletId = params.userWalletId, - cryptoCurrency = params.cryptoCurrency, + cryptoCurrency = cryptoCurrencyStatus.currency, apy = apy, ), ) } - @Suppress("MaximumLineLength") private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch( 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 processing = uiState.value is YieldSupplyUM.Processing - Timber.d( - "YIELD " + - "yieldSupplyStatus $yieldSupplyStatus " + - "tokenProtocolStatus $tokenProtocolStatus " + - "tokenPendingStatus $tokenPendingStatus " + - "isActive $isActive " + - "processing $processing " + - "isCryptoCurrencyStatusFromCache $isCryptoCurrencyStatusFromCache", - ) if (isCryptoCurrencyStatusFromCache && processing) { return@launch } - when { - tokenProtocolStatus != null && tokenPendingStatus != null -> { - showProcessing(tokenPendingStatus) - lastStatusCheckTimestamp = 0L - } - tokenProtocolStatus == YieldSupplyEnterStatus.Exit && isActive || - tokenProtocolStatus == YieldSupplyEnterStatus.Enter && !isActive -> { - if (lastStatusCheckTimestamp != 0L) { - if (SystemClock.elapsedRealtime() - lastStatusCheckTimestamp > MAX_STATUS_CHECK_LIMIT) { - loadStatus(cryptoCurrencyStatus) - lastStatusCheckTimestamp = 0L - } else { - showProcessing(tokenProtocolStatus) - } - } else { - showProcessing(tokenProtocolStatus) - lastStatusCheckTimestamp = SystemClock.elapsedRealtime() - } - } - else -> { - loadStatus(cryptoCurrencyStatus) - lastStatusCheckTimestamp = 0L - } + val pendingStatus = yieldSupplyEnterStatusUseCase( + userWalletId = userWallet.walletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull() + + if (pendingStatus != null) { + showProcessing(pendingStatus) + } else { + loadStatus(cryptoCurrencyStatus) } }.saveIn(loadStatusJobHolder) - private fun showProcessing(status: YieldSupplyEnterStatus) { + private fun showProcessing(status: YieldSupplyPendingStatus) { uiState.update { when (status) { - YieldSupplyEnterStatus.Enter -> YieldSupplyUM.Processing.Enter - YieldSupplyEnterStatus.Exit -> YieldSupplyUM.Processing.Exit + is YieldSupplyPendingStatus.Enter -> YieldSupplyUM.Processing.Enter + is YieldSupplyPendingStatus.Exit -> YieldSupplyUM.Processing.Exit } } fetchCurrencyWithDelay() @@ -249,11 +206,6 @@ internal class YieldSupplyModel @Inject constructor( private suspend fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) { val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus - yieldSupplyRepository.saveTokenProtocolStatus( - userWalletId = userWallet.walletId, - cryptoCurrency = cryptoCurrency, - yieldSupplyEnterStatus = null, - ) if (yieldSupplyStatus?.isActive == true) { loadActiveState( cryptoCurrencyStatus = cryptoCurrencyStatus, @@ -385,6 +337,5 @@ internal class YieldSupplyModel @Inject constructor( private companion object { const val PROCESSING_UPDATE_DELAY = 10_000L - const val MAX_STATUS_CHECK_LIMIT = 10_000L } } \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt index 6a2d3d2597..144e0dc546 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -21,7 +21,7 @@ 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.models.YieldSupplyPendingStatus import com.tangem.domain.yield.supply.usecase.* import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.impl.R @@ -245,18 +245,21 @@ internal class YieldSupplyStartEarningModel @Inject constructor( }, ) }, - ifRight = { - onStartEarningTransactionSuccess(yieldSupplyFeeUM) + ifRight = { txsData -> + onStartEarningTransactionSuccess(yieldSupplyFeeUM, txsData) }, ) } } - private suspend fun onStartEarningTransactionSuccess(yieldSupplyFeeUM: YieldSupplyFeeUM.Content) { - yieldSupplyRepository.saveTokenProtocolStatus( - userWalletId, - cryptoCurrency, - YieldSupplyEnterStatus.Enter, + private suspend fun onStartEarningTransactionSuccess( + yieldSupplyFeeUM: YieldSupplyFeeUM.Content, + txsData: List, + ) { + yieldSupplyRepository.saveTokenProtocolPendingStatus( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + yieldSupplyPendingStatus = YieldSupplyPendingStatus.Enter(txsData), ) val event = AnalyticsParam.TxSentFrom.Earning( blockchain = cryptoCurrency.network.name, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt index f327d6c82a..3a0b8b4398 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt @@ -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.YieldSupplyRepository 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.YieldSupplyStopEarningUseCase import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics @@ -167,18 +167,18 @@ internal class YieldSupplyStopEarningModel @Inject constructor( ) params.callback.onTransactionProgress(false) }, - ifRight = { - onStopEarningTransactionSuccess() + ifRight = { txData -> + onStopEarningTransactionSuccess(txData) }, ) } } - private suspend fun onStopEarningTransactionSuccess() { - yieldSupplyRepository.saveTokenProtocolStatus( - userWallet.walletId, - cryptoCurrency, - YieldSupplyEnterStatus.Exit, + private suspend fun onStopEarningTransactionSuccess(txId: String) { + yieldSupplyRepository.saveTokenProtocolPendingStatus( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrency, + yieldSupplyPendingStatus = YieldSupplyPendingStatus.Exit(listOf(txId)), ) analytics.send( YieldSupplyAnalytics.FundsWithdrawn( diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index fdc2399973..4dbe65cafc 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1365" +tangemBlockchainSdk = "develop-1366" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-573" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index 0e7eda2f34..a4f5188846 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -36,6 +36,9 @@ internal class WalletManagerFactoryCreator @Inject constructor( accountCreator = accountCreator, featureToggles = BlockchainFeatureToggles( isYieldSupplyEnabled = featureTogglesManager.isFeatureEnabled("YIELD_SUPPLY_FEATURE_ENABLED"), + isPendingTransactionsEnabled = featureTogglesManager.isFeatureEnabled( + "YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED", + ), ), blockchainDataStorage = blockchainDataStorage, loggers = listOf(blockchainSDKLogger),