diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 27f8c3c8e9..f6139be58a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -306,6 +306,26 @@
android:host="news"
android:scheme="tangem" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 7351bc519d..7f32d68de6 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
@@ -709,6 +709,16 @@ internal class ChildFactory @Inject constructor(
componentFactory = feedEntryComponentFactory,
)
}
+ is AppRoute.Earn -> {
+ createComponentChild(
+ context = context,
+ params = FeedEntryRoute.Earn(
+ preselectedEarnType = route.preselectedEarnType,
+ preselectedNetworkId = route.preselectedNetworkId,
+ ),
+ componentFactory = feedEntryComponentFactory,
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
index 3e9d268dd5..23e1f1f8d7 100644
--- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
+++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
@@ -6,11 +6,13 @@ import com.tangem.common.routing.DeepLinkRoute
import com.tangem.common.routing.DeepLinkScheme
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
+import com.tangem.features.feed.entry.deeplink.EarnDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.MarketsDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.MarketsTokenDetailDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.MarketsTokenExchangesDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.NewsDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
+import com.tangem.features.feed.entry.deeplink.YieldDeepLinkHandler
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
@@ -56,6 +58,8 @@ internal class DeepLinkFactory @Inject constructor(
private val marketsTokenExchangesDeepLink: MarketsTokenExchangesDeepLinkHandler.Factory,
private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory,
private val newsDeepLink: NewsDeepLinkHandler.Factory,
+ private val earnDeepLink: EarnDeepLinkHandler.Factory,
+ private val yieldDeepLink: YieldDeepLinkHandler.Factory,
) {
private val permittedAppRoute = MutableStateFlow(false)
@@ -162,6 +166,8 @@ internal class DeepLinkFactory @Inject constructor(
DeepLinkRoute.Promo.host -> promoDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.OnboardVisa.host -> onboardVisaDeepLink.create(deeplinkUri)
DeepLinkRoute.News.host -> newsDeepLink.create(queryParams)
+ DeepLinkRoute.Earn.host -> earnDeepLink.create(queryParams)
+ DeepLinkRoute.Yield.host -> yieldDeepLink.create(coroutineScope, queryParams)
else -> {
TangemLogger.i(
"""
diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
index 7a1c5173b5..0c50d75372 100644
--- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
+++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
@@ -4,11 +4,13 @@ import android.net.Uri
import com.tangem.common.routing.AppRoute
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
+import com.tangem.features.feed.entry.deeplink.EarnDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.MarketsDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.MarketsTokenDetailDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.MarketsTokenExchangesDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.NewsDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
+import com.tangem.features.feed.entry.deeplink.YieldDeepLinkHandler
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
@@ -92,6 +94,14 @@ class DeepLinkFactoryTest {
every { create(any()) } returns mockk()
}
+ private val earnDeepLinkFactory = mockk(relaxed = true) {
+ every { create(any()) } returns mockk()
+ }
+
+ private val yieldDeepLinkFactory = mockk(relaxed = true) {
+ every { create(any(), any()) } returns mockk()
+ }
+
private val marketsTokenExchangesDeepLinkFactory =
mockk(relaxed = true) {
every { create(any(), any()) } returns mockk()
@@ -122,6 +132,8 @@ class DeepLinkFactoryTest {
onboardVisaDeepLink = onboardVisaDeepLink,
newsDetailsDeepLink = newsDeeplink,
newsDeepLink = newsDeepLinkFactory,
+ earnDeepLink = earnDeepLinkFactory,
+ yieldDeepLink = yieldDeepLinkFactory,
)
@OptIn(ExperimentalCoroutinesApi::class)
@@ -439,18 +451,28 @@ class DeepLinkFactoryTest {
}
@Test
- fun `handleTangemDeepLinks routes news host to dedicated handler`() = runTest {
+ fun `handleTangemDeepLinks routes news, earn and yield hosts to dedicated handlers`() = runTest {
every { mockedUri.scheme } returns "tangem"
- every { mockedUri.host } returns "news"
every { mockedUri.query } returns null
every { mockedUri.queryParameterNames } returns emptySet()
every { mockedUri.getQueryParameter(any()) } returns null
deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet)
+
+ every { mockedUri.host } returns "news"
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
-
verify { newsDeepLinkFactory.create(eq(emptyMap())) }
+
+ every { mockedUri.host } returns "earn"
+ deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
+ advanceUntilIdle()
+ verify { earnDeepLinkFactory.create(eq(emptyMap())) }
+
+ every { mockedUri.host } returns "yield"
+ deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
+ advanceUntilIdle()
+ verify { yieldDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
}
@Test
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 3a6eac7c41..42bedf6b3e 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
@@ -17,6 +17,7 @@ import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.currency.CryptoCurrency
+import com.tangem.domain.models.earn.PreselectedEarnType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.serialization.SerializedBigDecimal
import com.tangem.domain.models.wallet.UserWalletId
@@ -494,4 +495,10 @@ sealed class AppRoute(val path: String) : Route {
data class News(
val categoryId: Int? = null,
) : AppRoute(path = "/news")
+
+ @Serializable
+ data class Earn(
+ val preselectedEarnType: PreselectedEarnType? = null,
+ val preselectedNetworkId: String? = null,
+ ) : AppRoute(path = "/earn")
}
\ No newline at end of file
diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt
index b9caabe5d0..0bdcdc9f27 100644
--- a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt
+++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt
@@ -75,6 +75,14 @@ sealed class DeepLinkRoute {
data object News : DeepLinkRoute() {
override val host: String = "news"
}
+
+ data object Earn : DeepLinkRoute() {
+ override val host: String = "earn"
+ }
+
+ data object Yield : DeepLinkRoute() {
+ override val host: String = "yield"
+ }
}
enum class DeepLinkScheme(val scheme: String) {
diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt
index 8f745a3b32..5196edd70a 100644
--- a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt
+++ b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt
@@ -20,4 +20,5 @@ object DeeplinkConst {
const val SECTION_KEY = "section"
const val CATEGORY_ID_KEY = "category_id"
const val NEWS_ID_KEY = "news_id"
+ const val EARN_TYPE_KEY = "earn_type"
}
\ No newline at end of file
diff --git a/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt b/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt
index bdcb99e48d..4e7aab6d8b 100644
--- a/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt
+++ b/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt
@@ -108,6 +108,31 @@ internal class DeepLinkBuilderTest {
assertThat(result).isEqualTo("${DeeplinkConst.TANGEM_SCHEME}://news?${DeeplinkConst.NEWS_ID_KEY}=20533")
}
+ @Test
+ fun `earn host with filters produces expected uri`() {
+ val result = deepLinkBuilder
+ .setAction("earn")
+ .addQueryParam(DeeplinkConst.EARN_TYPE_KEY, "yield")
+ .addQueryParam(DeeplinkConst.NETWORK_ID_KEY, "base")
+ .build()
+
+ assertThat(result).isEqualTo(
+ "${DeeplinkConst.TANGEM_SCHEME}://earn?${DeeplinkConst.EARN_TYPE_KEY}=yield" +
+ "&${DeeplinkConst.NETWORK_ID_KEY}=base",
+ )
+ }
+
+ @Test
+ fun `yield host with token and network produces expected uri`() {
+ val result = deepLinkBuilder
+ .setAction("yield")
+ .addQueryParam(DeeplinkConst.TOKEN_ID_KEY, "usd-coin")
+ .addQueryParam(DeeplinkConst.NETWORK_ID_KEY, "base")
+ .build()
+
+ assertThat(result).isEqualTo("${DeeplinkConst.TANGEM_SCHEME}://yield?token_id=usd-coin&network_id=base")
+ }
+
@Test
fun `GIVEN complex deep link WHEN build THEN should construct correct URI`() {
// GIVEN
diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/PreselectedEarnType.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/PreselectedEarnType.kt
new file mode 100644
index 0000000000..51d961c48a
--- /dev/null
+++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/PreselectedEarnType.kt
@@ -0,0 +1,14 @@
+package com.tangem.domain.models.earn
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+enum class PreselectedEarnType(val value: String) {
+ Staking("staking"),
+ Yield("yield"),
+ ;
+
+ companion object {
+ fun parse(value: String?): PreselectedEarnType? = entries.firstOrNull { it.value == value }
+ }
+}
\ No newline at end of file
diff --git a/features/feed/api/build.gradle.kts b/features/feed/api/build.gradle.kts
index 890d5aefad..b0a3a10ffc 100644
--- a/features/feed/api/build.gradle.kts
+++ b/features/feed/api/build.gradle.kts
@@ -22,6 +22,7 @@ dependencies {
/* Project - Domain */
implementation(projects.domain.core)
+ implementation(projects.domain.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.markets.models)
diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryRoute.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryRoute.kt
index 098b29f4da..38225e22f9 100644
--- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryRoute.kt
+++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryRoute.kt
@@ -5,6 +5,7 @@ import com.tangem.domain.markets.PreselectedMarketsInterval
import com.tangem.domain.markets.PreselectedMarketsOrder
import com.tangem.domain.markets.PreselectedTokenDetailsSection
import com.tangem.domain.markets.TokenMarketParams
+import com.tangem.domain.models.earn.PreselectedEarnType
import kotlinx.serialization.Serializable
@Serializable
@@ -39,4 +40,10 @@ sealed interface FeedEntryRoute {
@Serializable
data class NewsList(val preselectedCategoryId: Int? = null) : FeedEntryRoute
+
+ @Serializable
+ data class Earn(
+ val preselectedEarnType: PreselectedEarnType? = null,
+ val preselectedNetworkId: String? = null,
+ ) : FeedEntryRoute
}
\ No newline at end of file
diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/EarnDeepLinkHandler.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/EarnDeepLinkHandler.kt
new file mode 100644
index 0000000000..01c17aed53
--- /dev/null
+++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/EarnDeepLinkHandler.kt
@@ -0,0 +1,8 @@
+package com.tangem.features.feed.entry.deeplink
+
+interface EarnDeepLinkHandler {
+
+ interface Factory {
+ fun create(queryParams: Map): EarnDeepLinkHandler
+ }
+}
\ No newline at end of file
diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/YieldDeepLinkHandler.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/YieldDeepLinkHandler.kt
new file mode 100644
index 0000000000..5512452cb0
--- /dev/null
+++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/YieldDeepLinkHandler.kt
@@ -0,0 +1,10 @@
+package com.tangem.features.feed.entry.deeplink
+
+import kotlinx.coroutines.CoroutineScope
+
+interface YieldDeepLinkHandler {
+
+ interface Factory {
+ fun create(coroutineScope: CoroutineScope, queryParams: Map): YieldDeepLinkHandler
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts
index 7d4dc84f32..a9908d1787 100644
--- a/features/feed/impl/build.gradle.kts
+++ b/features/feed/impl/build.gradle.kts
@@ -112,4 +112,5 @@ dependencies {
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
+ testImplementation(deps.test.coroutine)
}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt
index 198cdac711..94c272503e 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt
@@ -21,10 +21,13 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
+import com.tangem.domain.models.earn.PreselectedEarnType
import com.tangem.domain.news.model.NewsListConfig
+import com.tangem.features.feed.components.earn.DefaultEarnComponent
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
+import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
import com.tangem.features.feed.entry.components.FeedEntryComponent
import com.tangem.features.feed.entry.components.FeedEntryRoute
import com.tangem.features.feed.model.FeedEntryModel
@@ -125,11 +128,19 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
}
override fun onOpenAllNews() {
- innerRouter.push(FeedEntryChildFactory.Child.NewsList())
+ innerRouter.push(
+ FeedEntryChildFactory.Child.NewsList(
+ params = buildNewsListParams(onBack = { onChildBack() }),
+ ),
+ )
}
override fun onOpenEarnPage() {
- innerRouter.push(FeedEntryChildFactory.Child.Earn)
+ innerRouter.push(
+ FeedEntryChildFactory.Child.Earn(
+ params = buildEarnParams(onBack = { onChildBack() }),
+ ),
+ )
}
override fun openSearch(source: String) {
@@ -264,12 +275,49 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
),
)
is FeedEntryRoute.NewsList -> FeedEntryChildFactory.Child.NewsList(
- preselectedCategoryId = entryRoute.preselectedCategoryId,
+ params = buildNewsListParams(
+ onBack = { router.pop() },
+ preselectedCategoryId = entryRoute.preselectedCategoryId,
+ ),
+ )
+ is FeedEntryRoute.Earn -> FeedEntryChildFactory.Child.Earn(
+ params = buildEarnParams(
+ onBack = { router.pop() },
+ preselectedEarnType = entryRoute.preselectedEarnType,
+ preselectedNetworkId = entryRoute.preselectedNetworkId,
+ ),
)
null -> FeedEntryChildFactory.Child.Feed
}
}
+ private fun buildNewsListParams(
+ onBack: () -> Unit,
+ preselectedCategoryId: Int? = null,
+ ): DefaultNewsListComponent.Params = DefaultNewsListComponent.Params(
+ onArticleClicked = { currentArticle, prefetchedArticles, paginationConfig ->
+ clickIntents.onArticleClick(
+ articleId = currentArticle,
+ preselectedArticlesId = prefetchedArticles,
+ screenSource = AnalyticsParam.ScreensSources.NewsList,
+ paginationConfig = paginationConfig,
+ )
+ },
+ onBackClick = onBack,
+ preselectedCategoryId = preselectedCategoryId,
+ )
+
+ private fun buildEarnParams(
+ onBack: () -> Unit,
+ preselectedEarnType: PreselectedEarnType? = null,
+ preselectedNetworkId: String? = null,
+ ): DefaultEarnComponent.Params = DefaultEarnComponent.Params(
+ onBackClick = onBack,
+ onSearchClicked = clickIntents::openSearch,
+ preselectedEarnType = preselectedEarnType,
+ preselectedNetworkId = preselectedNetworkId,
+ )
+
@AssistedFactory
interface Factory : FeedEntryComponent.Factory {
override fun create(context: AppComponentContext, entryRoute: FeedEntryRoute?): DefaultFeedEntryComponent
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt
index fb8c3a7f90..4ae731f39c 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt
@@ -17,7 +17,6 @@ import com.tangem.features.feed.components.market.details.portfolioblock.Portfol
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
-import com.tangem.features.feed.components.news.list.DefaultNewsListComponent.Params
import com.tangem.features.feed.components.search.DefaultSearchComponent
import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
@@ -53,7 +52,7 @@ internal class FeedEntryChildFactory @Inject constructor(
@Serializable
@Immutable
- data class NewsList(val preselectedCategoryId: Int? = null) : Child
+ data class NewsList(val params: DefaultNewsListComponent.Params) : Child
@Serializable
@Immutable
@@ -61,7 +60,7 @@ internal class FeedEntryChildFactory @Inject constructor(
@Serializable
@Immutable
- data object Earn : Child
+ data class Earn(val params: DefaultEarnComponent.Params) : Child
@Serializable
@Immutable
@@ -113,18 +112,7 @@ internal class FeedEntryChildFactory @Inject constructor(
is Child.NewsList -> {
DefaultNewsListComponent(
appComponentContext = appComponentContext,
- params = Params(
- onArticleClicked = { currentArticle, prefetchedArticles, paginationConfig ->
- feedEntryClickIntents.onArticleClick(
- articleId = currentArticle,
- preselectedArticlesId = prefetchedArticles,
- screenSource = AnalyticsParam.ScreensSources.NewsList,
- paginationConfig = paginationConfig,
- )
- },
- onBackClick = onBackClicked,
- preselectedCategoryId = child.preselectedCategoryId,
- ),
+ params = child.params,
)
}
Child.Feed -> {
@@ -139,10 +127,7 @@ internal class FeedEntryChildFactory @Inject constructor(
is Child.Earn -> {
DefaultEarnComponent(
appComponentContext = appComponentContext,
- params = DefaultEarnComponent.Params(
- onBackClick = onBackClicked,
- onSearchClicked = feedEntryClickIntents::openSearch,
- ),
+ params = child.params,
addToPortfolioComponentFactory = addToPortfolioComponentFactory,
)
}
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt
index bdef4f7ffe..9e4094404f 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt
@@ -34,7 +34,9 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
+import com.tangem.domain.models.earn.PreselectedEarnType
import com.tangem.features.feed.components.feed.FeedBottomSheetRoute
+import kotlinx.serialization.Serializable
import com.tangem.features.feed.model.earn.EarnModel
import com.tangem.features.feed.model.earn.analytics.EarnSource
import com.tangem.features.feed.ui.components.FeedSearchBar
@@ -151,8 +153,11 @@ internal class DefaultEarnComponent(
)
}
+ @Serializable
data class Params(
val onBackClick: () -> Unit,
val onSearchClicked: (source: String) -> Unit,
+ val preselectedEarnType: PreselectedEarnType? = null,
+ val preselectedNetworkId: String? = null,
)
}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultEarnDeepLinkHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultEarnDeepLinkHandler.kt
new file mode 100644
index 0000000000..4609b29cfb
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultEarnDeepLinkHandler.kt
@@ -0,0 +1,38 @@
+package com.tangem.features.feed.deeplink
+
+import com.tangem.common.routing.AppRoute
+import com.tangem.common.routing.AppRouter
+import com.tangem.common.routing.deeplink.DeeplinkConst.EARN_TYPE_KEY
+import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
+import com.tangem.domain.models.earn.PreselectedEarnType
+import com.tangem.features.feed.entry.deeplink.EarnDeepLinkHandler
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+internal class DefaultEarnDeepLinkHandler @AssistedInject constructor(
+ @Assisted private val queryParams: Map,
+ private val appRouter: AppRouter,
+) : EarnDeepLinkHandler {
+
+ init {
+ handleDeepLink()
+ }
+
+ private fun handleDeepLink() {
+ val earnType = PreselectedEarnType.parse(queryParams[EARN_TYPE_KEY])
+ val networkId = queryParams[NETWORK_ID_KEY]?.takeIf { it.isNotBlank() }
+
+ appRouter.push(
+ AppRoute.Earn(
+ preselectedEarnType = earnType,
+ preselectedNetworkId = networkId,
+ ),
+ )
+ }
+
+ @AssistedFactory
+ interface Factory : EarnDeepLinkHandler.Factory {
+ override fun create(queryParams: Map): DefaultEarnDeepLinkHandler
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultYieldDeepLinkHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultYieldDeepLinkHandler.kt
new file mode 100644
index 0000000000..ee24674898
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultYieldDeepLinkHandler.kt
@@ -0,0 +1,107 @@
+package com.tangem.features.feed.deeplink
+
+import com.tangem.common.routing.AppRoute
+import com.tangem.common.routing.AppRouter
+import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
+import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
+import com.tangem.domain.models.earn.PreselectedEarnType
+import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
+import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
+import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
+import com.tangem.domain.yield.supply.models.YieldSupplyAvailability
+import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase
+import com.tangem.features.feed.entry.deeplink.YieldDeepLinkHandler
+import com.tangem.utils.logging.TangemLogger
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+internal class DefaultYieldDeepLinkHandler @AssistedInject constructor(
+ @Assisted private val scope: CoroutineScope,
+ @Assisted private val queryParams: Map,
+ private val appRouter: AppRouter,
+ private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
+ private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
+ private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase,
+) : YieldDeepLinkHandler {
+
+ init {
+ handleDeepLink()
+ }
+
+ private fun handleDeepLink() {
+ val tokenId = queryParams[TOKEN_ID_KEY]?.takeIf { it.isNotBlank() }
+ val networkId = queryParams[NETWORK_ID_KEY]?.takeIf { it.isNotBlank() }
+
+ if (tokenId == null || networkId == null) {
+ TangemLogger.i("Yield deeplink: missing token_id or network_id; falling back to earn yield list")
+ pushFallback(networkId)
+ return
+ }
+
+ scope.launch {
+ val userWallet = getSelectedWalletSyncUseCase().getOrNull()
+ if (userWallet == null) {
+ TangemLogger.e("Yield deeplink: no selected user wallet")
+ pushFallback(networkId)
+ return@launch
+ }
+
+ val cryptoCurrency = multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
+ params = MultiWalletCryptoCurrenciesProducer.Params(userWallet.walletId),
+ )
+ .orEmpty()
+ .firstOrNull { currency ->
+ currency.network.rawId.equals(networkId, ignoreCase = true) &&
+ currency.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
+ }
+
+ if (cryptoCurrency == null) {
+ TangemLogger.i(
+ """
+ Yield deeplink: token not in selected wallet
+ |- $TOKEN_ID_KEY: $tokenId
+ |- $NETWORK_ID_KEY: $networkId
+ """.trimIndent(),
+ )
+ pushFallback(networkId)
+ return@launch
+ }
+
+ val availability = yieldSupplyGetAvailabilityUseCase(cryptoCurrency).getOrNull()
+ val available = availability as? YieldSupplyAvailability.Available
+ if (available == null) {
+ TangemLogger.i("Yield deeplink: yield not available for ${cryptoCurrency.name}")
+ pushFallback(networkId)
+ return@launch
+ }
+
+ appRouter.push(
+ AppRoute.YieldSupplyEntry(
+ userWalletId = userWallet.walletId,
+ cryptoCurrency = cryptoCurrency,
+ apy = available.apy,
+ ),
+ )
+ }
+ }
+
+ private fun pushFallback(networkId: String?) {
+ appRouter.push(
+ AppRoute.Earn(
+ preselectedEarnType = PreselectedEarnType.Yield,
+ preselectedNetworkId = networkId,
+ ),
+ )
+ }
+
+ @AssistedFactory
+ interface Factory : YieldDeepLinkHandler.Factory {
+ override fun create(
+ coroutineScope: CoroutineScope,
+ queryParams: Map,
+ ): DefaultYieldDeepLinkHandler
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt
index 05f42eb535..0af064279f 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt
@@ -1,9 +1,13 @@
package com.tangem.features.feed.deeplink.di
+import com.tangem.features.feed.deeplink.DefaultEarnDeepLinkHandler
import com.tangem.features.feed.deeplink.DefaultNewsDeepLinkHandler
import com.tangem.features.feed.deeplink.DefaultNewsDetailsDeepLinkHandler
+import com.tangem.features.feed.deeplink.DefaultYieldDeepLinkHandler
+import com.tangem.features.feed.entry.deeplink.EarnDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.NewsDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
+import com.tangem.features.feed.entry.deeplink.YieldDeepLinkHandler
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -23,4 +27,12 @@ internal interface FeedDeepLinkModule {
@Binds
@Singleton
fun bindNewsDeepLinkHandlerFactory(impl: DefaultNewsDeepLinkHandler.Factory): NewsDeepLinkHandler.Factory
+
+ @Binds
+ @Singleton
+ fun bindEarnDeepLinkHandlerFactory(impl: DefaultEarnDeepLinkHandler.Factory): EarnDeepLinkHandler.Factory
+
+ @Binds
+ @Singleton
+ fun bindYieldDeepLinkHandlerFactory(impl: DefaultYieldDeepLinkHandler.Factory): YieldDeepLinkHandler.Factory
}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt
index d795a1289d..ae1280b2cf 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt
@@ -23,6 +23,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.earn.EarnNetworks
import com.tangem.domain.models.earn.EarnTokenWithCurrency
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
+import com.tangem.domain.models.earn.PreselectedEarnType
import com.tangem.features.feed.components.earn.DefaultEarnComponent
import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent
import com.tangem.features.feed.components.earn.EarnTypeFilterComponent
@@ -108,7 +109,8 @@ internal class EarnModel @Inject constructor(
init {
updateInitialState()
fetchEarnNetworks()
- subscribeOnStoredFilters()
+ fetchTopEarnTokens()
+ subscribeOnActiveFilters()
subscribeOnNetworks()
subscribeOnBatchFlow()
subscribeToMostlyUsed()
@@ -147,6 +149,45 @@ internal class EarnModel @Inject constructor(
)
}
+ private fun activeFilters(): Flow {
+ val deeplink = buildDeeplinkFilter() ?: return getEarnFilterUseCase()
+ return getEarnFilterUseCase().drop(1).onStart { emit(deeplink) }
+ }
+
+ private fun buildDeeplinkFilter(): EarnFilter? {
+ val type = params.preselectedEarnType?.toEarnFilterType()
+ val networkId = params.preselectedNetworkId?.takeIf { it.isNotBlank() }
+ if (type == null && networkId == null) return null
+ return EarnFilter(
+ earnFilterType = type ?: EarnFilterType.ALL,
+ earnFilterNetwork = networkId
+ ?.let { EarnFilterNetwork.Specific(id = it, symbol = "", fullName = it, isSelected = true) }
+ ?: EarnFilterNetwork.AllNetworks(isSelected = true),
+ )
+ }
+
+ private fun EarnFilter.resolveAgainst(networks: EarnNetworks): EarnFilter {
+ val specific = earnFilterNetwork as? EarnFilterNetwork.Specific ?: return this
+ if (specific.symbol.isNotEmpty()) return this
+ val loaded = networks.getOrNull()?.takeIf { it.isNotEmpty() } ?: return this
+ val match = loaded.firstOrNull { it.networkId.equals(specific.id, ignoreCase = true) }
+ return copy(
+ earnFilterNetwork = match?.let { earnNetwork ->
+ EarnFilterNetwork.Specific(
+ isSelected = true,
+ id = earnNetwork.networkId,
+ symbol = earnNetwork.symbol,
+ fullName = earnNetwork.fullName,
+ )
+ } ?: EarnFilterNetwork.AllNetworks(isSelected = true),
+ )
+ }
+
+ private fun PreselectedEarnType.toEarnFilterType(): EarnFilterType = when (this) {
+ PreselectedEarnType.Staking -> EarnFilterType.STAKING
+ PreselectedEarnType.Yield -> EarnFilterType.YIELD
+ }
+
private fun subscribeOnBatchFlow() {
combine(
batchFlowManager.uiItems,
@@ -193,18 +234,14 @@ internal class EarnModel @Inject constructor(
}
}
- private fun subscribeOnStoredFilters() {
+ private fun subscribeOnActiveFilters() {
modelScope.launch(dispatchers.default) {
- combine(
- getEarnFilterUseCase(),
- earnNetworks,
- ) { filter, networks ->
- val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType)
- val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork)
+ combine(activeFilters(), earnNetworks) { filter, networks ->
+ val resolved = filter.resolveAgainst(networks)
stateController.update(
EarnFilterSelectedStateTransformer(
- filterType = typeFilterUM,
- filterNetwork = networkFilterUM,
+ filterType = EarnFilterTypeConverter().convert(resolved.earnFilterType),
+ filterNetwork = EarnFilterNetworkConverter().convert(resolved.earnFilterNetwork),
earnNetworks = networks,
),
)
diff --git a/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultEarnDeepLinkHandlerTest.kt b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultEarnDeepLinkHandlerTest.kt
new file mode 100644
index 0000000000..bdaf14d99c
--- /dev/null
+++ b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultEarnDeepLinkHandlerTest.kt
@@ -0,0 +1,129 @@
+package com.tangem.features.feed.deeplink
+
+import com.tangem.common.routing.AppRoute
+import com.tangem.common.routing.AppRouter
+import com.tangem.common.routing.deeplink.DeeplinkConst.EARN_TYPE_KEY
+import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
+import com.tangem.domain.models.earn.PreselectedEarnType
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.verify
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+class DefaultEarnDeepLinkHandlerTest {
+
+ private val appRouter: AppRouter = mockk()
+
+ @BeforeEach
+ fun setUp() {
+ every { appRouter.push(any(), any()) } just Runs
+ }
+
+ @Test
+ fun `no params opens earn list with no filters`() {
+ DefaultEarnDeepLinkHandler(queryParams = emptyMap(), appRouter = appRouter)
+
+ verify {
+ appRouter.push(
+ AppRoute.Earn(preselectedEarnType = null, preselectedNetworkId = null),
+ any(),
+ )
+ }
+ }
+
+ @Test
+ fun `staking earnType is parsed`() {
+ DefaultEarnDeepLinkHandler(
+ queryParams = mapOf(EARN_TYPE_KEY to "staking"),
+ appRouter = appRouter,
+ )
+
+ verify {
+ appRouter.push(
+ AppRoute.Earn(preselectedEarnType = PreselectedEarnType.Staking, preselectedNetworkId = null),
+ any(),
+ )
+ }
+ }
+
+ @Test
+ fun `yield earnType is parsed`() {
+ DefaultEarnDeepLinkHandler(
+ queryParams = mapOf(EARN_TYPE_KEY to "yield"),
+ appRouter = appRouter,
+ )
+
+ verify {
+ appRouter.push(
+ AppRoute.Earn(preselectedEarnType = PreselectedEarnType.Yield, preselectedNetworkId = null),
+ any(),
+ )
+ }
+ }
+
+ @Test
+ fun `invalid earnType is silently dropped`() {
+ DefaultEarnDeepLinkHandler(
+ queryParams = mapOf(EARN_TYPE_KEY to "bogus"),
+ appRouter = appRouter,
+ )
+
+ verify {
+ appRouter.push(
+ AppRoute.Earn(preselectedEarnType = null, preselectedNetworkId = null),
+ any(),
+ )
+ }
+ }
+
+ @Test
+ fun `networkId is passed through`() {
+ DefaultEarnDeepLinkHandler(
+ queryParams = mapOf(NETWORK_ID_KEY to "base"),
+ appRouter = appRouter,
+ )
+
+ verify {
+ appRouter.push(
+ AppRoute.Earn(preselectedEarnType = null, preselectedNetworkId = "base"),
+ any(),
+ )
+ }
+ }
+
+ @Test
+ fun `blank networkId is dropped`() {
+ DefaultEarnDeepLinkHandler(
+ queryParams = mapOf(NETWORK_ID_KEY to " "),
+ appRouter = appRouter,
+ )
+
+ verify {
+ appRouter.push(
+ AppRoute.Earn(preselectedEarnType = null, preselectedNetworkId = null),
+ any(),
+ )
+ }
+ }
+
+ @Test
+ fun `earnType and networkId together`() {
+ DefaultEarnDeepLinkHandler(
+ queryParams = mapOf(EARN_TYPE_KEY to "yield", NETWORK_ID_KEY to "base"),
+ appRouter = appRouter,
+ )
+
+ verify {
+ appRouter.push(
+ AppRoute.Earn(
+ preselectedEarnType = PreselectedEarnType.Yield,
+ preselectedNetworkId = "base",
+ ),
+ any(),
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultYieldDeepLinkHandlerTest.kt b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultYieldDeepLinkHandlerTest.kt
new file mode 100644
index 0000000000..b9f6992504
--- /dev/null
+++ b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultYieldDeepLinkHandlerTest.kt
@@ -0,0 +1,247 @@
+package com.tangem.features.feed.deeplink
+
+import arrow.core.Either
+import com.tangem.common.routing.AppRoute
+import com.tangem.common.routing.AppRouter
+import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
+import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
+import com.tangem.domain.models.currency.CryptoCurrency
+import com.tangem.domain.models.earn.PreselectedEarnType
+import com.tangem.domain.models.network.Network
+import com.tangem.domain.models.wallet.UserWallet
+import com.tangem.domain.models.wallet.UserWalletId
+import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
+import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
+import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
+import com.tangem.domain.yield.supply.models.YieldSupplyAvailability
+import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase
+import com.tangem.utils.logging.TangemLogger
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.verify
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class DefaultYieldDeepLinkHandlerTest {
+
+ private val appRouter: AppRouter = mockk()
+ private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase = mockk()
+ private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier = mockk()
+ private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase = mockk()
+
+ private val tokenId = "usd-coin"
+ private val networkId = "base"
+
+ @BeforeEach
+ fun setUp() {
+ mockkObject(TangemLogger)
+ every { appRouter.push(any(), any()) } just Runs
+ }
+
+ @Test
+ fun `missing token_id falls back to earn yield`() = runTest {
+ handle(
+ scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)),
+ queryParams = mapOf(NETWORK_ID_KEY to networkId),
+ )
+
+ verify {
+ appRouter.push(
+ AppRoute.Earn(
+ preselectedEarnType = PreselectedEarnType.Yield,
+ preselectedNetworkId = networkId,
+ ),
+ any(),
+ )
+ }
+ }
+
+ @Test
+ fun `missing network_id falls back to earn yield`() = runTest {
+ handle(
+ scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)),
+ queryParams = mapOf(TOKEN_ID_KEY to tokenId),
+ )
+
+ verify {
+ appRouter.push(
+ AppRoute.Earn(
+ preselectedEarnType = PreselectedEarnType.Yield,
+ preselectedNetworkId = null,
+ ),
+ any(),
+ )
+ }
+ }
+
+ @Test
+ fun `no selected wallet falls back to earn yield`() = runTest {
+ every { getSelectedWalletSyncUseCase() } returns Either.Left(mockk())
+
+ handle(
+ scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)),
+ queryParams = mapOf(TOKEN_ID_KEY to tokenId, NETWORK_ID_KEY to networkId),
+ )
+ advanceUntilIdle()
+
+ verify {
+ appRouter.push(
+ AppRoute.Earn(
+ preselectedEarnType = PreselectedEarnType.Yield,
+ preselectedNetworkId = networkId,
+ ),
+ any(),
+ )
+ }
+ }
+
+ @Test
+ fun `token not in wallet falls back to earn yield`() = runTest {
+ val walletId = UserWalletId("011")
+ val wallet = mockk { every { this@mockk.walletId } returns walletId }
+ every { getSelectedWalletSyncUseCase() } returns Either.Right(wallet)
+ coEvery {
+ multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
+ MultiWalletCryptoCurrenciesProducer.Params(walletId),
+ )
+ } returns emptySet()
+
+ handle(
+ scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)),
+ queryParams = mapOf(TOKEN_ID_KEY to tokenId, NETWORK_ID_KEY to networkId),
+ )
+ advanceUntilIdle()
+
+ verify {
+ appRouter.push(
+ AppRoute.Earn(
+ preselectedEarnType = PreselectedEarnType.Yield,
+ preselectedNetworkId = networkId,
+ ),
+ any(),
+ )
+ }
+ }
+
+ @Test
+ fun `eligible token with yield available pushes YieldSupplyEntry`() = runTest {
+ val walletId = UserWalletId("011")
+ val wallet = mockk { every { this@mockk.walletId } returns walletId }
+ val currency = mockEligibleCurrency()
+ every { getSelectedWalletSyncUseCase() } returns Either.Right(wallet)
+ coEvery {
+ multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
+ MultiWalletCryptoCurrenciesProducer.Params(walletId),
+ )
+ } returns setOf(currency)
+ coEvery { yieldSupplyGetAvailabilityUseCase(currency) } returns
+ Either.Right(YieldSupplyAvailability.Available(apy = "2.66"))
+
+ handle(
+ scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)),
+ queryParams = mapOf(TOKEN_ID_KEY to tokenId, NETWORK_ID_KEY to networkId),
+ )
+ advanceUntilIdle()
+
+ verify {
+ appRouter.push(
+ match {
+ it.userWalletId == walletId && it.cryptoCurrency === currency && it.apy == "2.66"
+ },
+ any(),
+ )
+ }
+ }
+
+ @Test
+ fun `eligible token with yield unavailable falls back to earn yield`() = runTest {
+ val walletId = UserWalletId("011")
+ val wallet = mockk { every { this@mockk.walletId } returns walletId }
+ val currency = mockEligibleCurrency()
+ every { getSelectedWalletSyncUseCase() } returns Either.Right(wallet)
+ coEvery {
+ multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
+ MultiWalletCryptoCurrenciesProducer.Params(walletId),
+ )
+ } returns setOf(currency)
+ coEvery { yieldSupplyGetAvailabilityUseCase(currency) } returns
+ Either.Right(YieldSupplyAvailability.Unavailable)
+
+ handle(
+ scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)),
+ queryParams = mapOf(TOKEN_ID_KEY to tokenId, NETWORK_ID_KEY to networkId),
+ )
+ advanceUntilIdle()
+
+ verify {
+ appRouter.push(
+ AppRoute.Earn(
+ preselectedEarnType = PreselectedEarnType.Yield,
+ preselectedNetworkId = networkId,
+ ),
+ any(),
+ )
+ }
+ }
+
+ @Test
+ fun `eligible token with availability lookup error falls back to earn yield`() = runTest {
+ val walletId = UserWalletId("011")
+ val wallet = mockk { every { this@mockk.walletId } returns walletId }
+ val currency = mockEligibleCurrency()
+ every { getSelectedWalletSyncUseCase() } returns Either.Right(wallet)
+ coEvery {
+ multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
+ MultiWalletCryptoCurrenciesProducer.Params(walletId),
+ )
+ } returns setOf(currency)
+ coEvery { yieldSupplyGetAvailabilityUseCase(currency) } returns
+ Either.Left(IllegalStateException("api 500"))
+
+ handle(
+ scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)),
+ queryParams = mapOf(TOKEN_ID_KEY to tokenId, NETWORK_ID_KEY to networkId),
+ )
+ advanceUntilIdle()
+
+ verify {
+ appRouter.push(
+ AppRoute.Earn(
+ preselectedEarnType = PreselectedEarnType.Yield,
+ preselectedNetworkId = networkId,
+ ),
+ any(),
+ )
+ }
+ }
+
+ /** Builds a [CryptoCurrency] mock whose `network.rawId` and `id.rawCurrencyId` match the test's
+ * [tokenId] / [networkId] — i.e. the supplier-lookup predicate inside the handler returns it. */
+ private fun mockEligibleCurrency(): CryptoCurrency = mockk(relaxed = true) {
+ every { network } returns mockk(relaxed = true) { every { rawId } returns networkId }
+ every { id } returns mockk(relaxed = true) {
+ every { rawCurrencyId } returns CryptoCurrency.RawID(tokenId)
+ }
+ }
+
+ private fun handle(scope: CoroutineScope, queryParams: Map) {
+ DefaultYieldDeepLinkHandler(
+ scope = scope,
+ queryParams = queryParams,
+ appRouter = appRouter,
+ getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
+ yieldSupplyGetAvailabilityUseCase = yieldSupplyGetAvailabilityUseCase,
+ )
+ }
+}
\ No newline at end of file