Updated on 2026-08-14
This commit is contained in:
parent
7f2f051415
commit
3d5a8654a5
23 changed files with 783 additions and 35 deletions
|
|
@ -306,6 +306,26 @@
|
|||
android:host="news"
|
||||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="earn"
|
||||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="yield"
|
||||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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<EarnDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create(any()) } returns mockk()
|
||||
}
|
||||
|
||||
private val yieldDeepLinkFactory = mockk<YieldDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create(any(), any()) } returns mockk()
|
||||
}
|
||||
|
||||
private val marketsTokenExchangesDeepLinkFactory =
|
||||
mockk<MarketsTokenExchangesDeepLinkHandler.Factory>(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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.features.feed.entry.deeplink
|
||||
|
||||
interface EarnDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(queryParams: Map<String, String>): EarnDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String>): YieldDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -112,4 +112,5 @@ dependencies {
|
|||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.coroutine)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<String, String>,
|
||||
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<String, String>): DefaultEarnDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String>,
|
||||
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<String, String>,
|
||||
): DefaultYieldDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<EarnFilter> {
|
||||
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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UserWallet> { 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<UserWallet> { 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<AppRoute.YieldSupplyEntry> {
|
||||
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<UserWallet> { 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<UserWallet> { 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<Network>(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<String, String>) {
|
||||
DefaultYieldDeepLinkHandler(
|
||||
scope = scope,
|
||||
queryParams = queryParams,
|
||||
appRouter = appRouter,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
yieldSupplyGetAvailabilityUseCase = yieldSupplyGetAvailabilityUseCase,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue