From 236794fa007667db606737e9b5f3a544e2ff9723 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 15 Apr 2026 13:53:26 +0500 Subject: [PATCH] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 11 +++ .../tangem/tap/routing/utils/ChildFactory.kt | 8 +- .../tap/routing/utils/DeepLinkFactory.kt | 5 +- .../tap/routing/utils/DeepLinkFactoryTest.kt | 11 ++- .../com/tangem/common/routing/AppRoute.kt | 11 ++- .../tangem/common/routing/DeepLinkRoute.kt | 4 + .../common/routing/deeplink/DeeplinkConst.kt | 3 + .../markets/PreselectedMarketsInterval.kt | 15 ++++ .../domain/markets/PreselectedMarketsOrder.kt | 17 ++++ .../markets/PreselectedTokenDetailsSection.kt | 13 +++ .../feed/entry/components/FeedEntryRoute.kt | 11 ++- .../entry/deeplink/MarketsDeepLinkHandler.kt | 2 +- .../MarketsTokenExchangesDeepLinkHandler.kt | 10 +++ .../components/DefaultFeedEntryComponent.kt | 32 ++++++- .../DefaultMarketsTokenDetailsComponent.kt | 4 + .../list/DefaultMarketsTokenListComponent.kt | 2 + .../deeplink/DefaultMarketsDeepLinkHandler.kt | 15 +++- ...efaultMarketsTokenDetailDeepLinkHandler.kt | 14 ++- ...ultMarketsTokenExchangesDeepLinkHandler.kt | 88 +++++++++++++++++++ .../feed/deeplink/di/MarketsDeepLinkModule.kt | 8 ++ .../details/MarketsTokenDetailsModel.kt | 31 +++++++ .../model/market/list/MarketsListModel.kt | 1 + .../statemanager/MarketsListUMStateManager.kt | 3 +- .../detailed/MarketsTokenDetailsContent.kt | 7 ++ .../components/TokenMarketDetailsBody.kt | 11 ++- .../detailed/state/MarketsTokenDetailsUM.kt | 8 +- 26 files changed, 329 insertions(+), 16 deletions(-) create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PreselectedMarketsInterval.kt create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PreselectedMarketsOrder.kt create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PreselectedTokenDetailsSection.kt create mode 100644 features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/MarketsTokenExchangesDeepLinkHandler.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenExchangesDeepLinkHandler.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2ef3c24014..95ce7f4c75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -232,6 +232,17 @@ 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 afff976cd4..588ae2ef38 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 @@ -195,6 +195,9 @@ internal class ChildFactory @Inject constructor( source = params.source, ) }, + preselectedSection = route.preselectedSection, + shouldOpenExchanges = route.shouldOpenExchanges, + exchangesCount = route.exchangesCount, ), componentFactory = feedEntryComponentFactory, ) @@ -458,7 +461,10 @@ internal class ChildFactory @Inject constructor( is AppRoute.Markets -> { createComponentChild( context = context, - params = FeedEntryRoute.MarketTokenList, + params = FeedEntryRoute.MarketTokenList( + preselectedOrder = route.preselectedOrder, + preselectedInterval = route.preselectedInterval, + ), 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 fb1acbcd80..492d1d23ae 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 @@ -8,6 +8,7 @@ import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler 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.NewsDetailsDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler @@ -51,6 +52,7 @@ internal class DeepLinkFactory @Inject constructor( private val swapDeepLink: SwapDeepLinkHandler.Factory, private val promoDeepLink: PromoDeeplinkHandler.Factory, private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory, + private val marketsTokenExchangesDeepLink: MarketsTokenExchangesDeepLinkHandler.Factory, private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory, ) { private val permittedAppRoute = MutableStateFlow(false) @@ -148,8 +150,9 @@ internal class DeepLinkFactory @Inject constructor( isFromOnNewIntent = isFromOnNewIntent, ) DeepLinkRoute.Staking.host -> stakingDeepLink.create(coroutineScope, queryParams) - DeepLinkRoute.Markets.host -> marketsDeepLink.create() + DeepLinkRoute.Markets.host -> marketsDeepLink.create(queryParams) DeepLinkRoute.MarketTokenDetail.host -> marketsTokenDetailDeepLink.create(coroutineScope, queryParams) + DeepLinkRoute.TokenExchanges.host -> marketsTokenExchangesDeepLink.create(coroutineScope, queryParams) DeepLinkRoute.Buy.host -> buyDeepLink.create() DeepLinkRoute.Sell.host -> sellDeepLink.create() DeepLinkRoute.Swap.host -> swapDeepLink.create() 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 0cb5e4db8f..43ba7a2555 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 @@ -6,6 +6,7 @@ import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler 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.NewsDetailsDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler @@ -55,7 +56,7 @@ class DeepLinkFactoryTest { every { create(any(), any()) } returns mockk() } private val marketsDeepLinkFactory = mockk(relaxed = true) { - every { create() } returns mockk() + every { create(any()) } returns mockk() } private val marketsTokenDetailDeepLinkFactory = mockk(relaxed = true) { every { create(any(), any()) } returns mockk() @@ -86,6 +87,11 @@ class DeepLinkFactoryTest { every { create(any(), any()) } returns mockk() } + private val marketsTokenExchangesDeepLinkFactory = + mockk(relaxed = true) { + every { create(any(), any()) } returns mockk() + } + private val mockedUri = mockk(relaxed = true) private val isFromOnNewIntent: Boolean = false @@ -103,6 +109,7 @@ class DeepLinkFactoryTest { stakingDeepLink = stakingDeepLinkFactory, marketsDeepLink = marketsDeepLinkFactory, marketsTokenDetailDeepLink = marketsTokenDetailDeepLinkFactory, + marketsTokenExchangesDeepLink = marketsTokenExchangesDeepLinkFactory, buyDeepLink = buyDeepLinkFactory, sellDeepLink = sellDeepLinkFactory, swapDeepLink = swapDeepLinkFactory, @@ -308,7 +315,7 @@ class DeepLinkFactoryTest { every { mockedUri.host } returns "markets" deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() - verify { marketsDeepLinkFactory.create() } + verify { marketsDeepLinkFactory.create(any()) } // Test Sell every { mockedUri.host } returns "sell" 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 6fba5a36d5..e78861b261 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 @@ -10,6 +10,9 @@ import com.tangem.common.routing.entity.InitScreenLaunchMode import com.tangem.core.decompose.navigation.Route import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.feedback.models.WalletMetaInfo +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.account.Account import com.tangem.domain.models.account.AccountId @@ -253,7 +256,10 @@ sealed class AppRoute(val path: String) : Route { ) : AppRoute(path = "/wallet_hardware_backup/${userWalletId.stringValue}") @Serializable - data object Markets : AppRoute(path = "/markets") + data class Markets( + val preselectedOrder: PreselectedMarketsOrder? = null, + val preselectedInterval: PreselectedMarketsInterval? = null, + ) : AppRoute(path = "/markets") @Serializable data class MarketsTokenDetails( @@ -261,6 +267,9 @@ sealed class AppRoute(val path: String) : Route { val appCurrency: AppCurrency, val shouldShowPortfolio: Boolean, val analyticsParams: AnalyticsParams? = null, + val preselectedSection: PreselectedTokenDetailsSection? = null, + val shouldOpenExchanges: Boolean = false, + val exchangesCount: Int? = null, ) : AppRoute(path = "/markets_token_details/${token.id}/$shouldShowPortfolio") { @Serializable 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 d57eb5e7f0..359839b059 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 @@ -60,6 +60,10 @@ sealed class DeepLinkRoute { override val host: String = "promo" } + data object TokenExchanges : DeepLinkRoute() { + override val host: String = "token_exchanges" + } + data object OnboardVisa : DeepLinkRoute() { override val host: String = "onboard-visa" } 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 630b68ded8..6fe8031620 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 @@ -15,4 +15,7 @@ object DeeplinkConst { const val REF_KEY = "ref" const val CAMPAIGN_KEY = "campaign" const val NAME_KEY = "name" + const val ORDER_KEY = "order" + const val INTERVAL_KEY = "interval" + const val SECTION_KEY = "section" } \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PreselectedMarketsInterval.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PreselectedMarketsInterval.kt new file mode 100644 index 0000000000..d87f2342c1 --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PreselectedMarketsInterval.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.markets + +import kotlinx.serialization.Serializable + +@Serializable +enum class PreselectedMarketsInterval(val value: String) { + H24("24h"), + W1("1w"), + D30("30d"), + ; + + companion object { + fun parse(value: String?): PreselectedMarketsInterval? = entries.firstOrNull { it.value == value } + } +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PreselectedMarketsOrder.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PreselectedMarketsOrder.kt new file mode 100644 index 0000000000..bc2cb04640 --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PreselectedMarketsOrder.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.markets + +import kotlinx.serialization.Serializable + +@Serializable +enum class PreselectedMarketsOrder(val value: String) { + Rating("rating"), + Trending("trending"), + Buyers("buyers"), + Gainers("gainers"), + Losers("losers"), + ; + + companion object { + fun parse(value: String?): PreselectedMarketsOrder? = entries.firstOrNull { it.value == value } + } +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PreselectedTokenDetailsSection.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PreselectedTokenDetailsSection.kt new file mode 100644 index 0000000000..008a3b086e --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PreselectedTokenDetailsSection.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.markets + +import kotlinx.serialization.Serializable + +@Serializable +enum class PreselectedTokenDetailsSection(val value: String) { + News("news"), + ; + + companion object { + fun parse(value: String?): PreselectedTokenDetailsSection? = entries.firstOrNull { it.value == value } + } +} \ No newline at end of file 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 7c9ffb7c0a..ab97474343 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 @@ -1,6 +1,9 @@ package com.tangem.features.feed.entry.components import com.tangem.domain.appcurrency.model.AppCurrency +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 kotlinx.serialization.Serializable @@ -13,6 +16,9 @@ sealed interface FeedEntryRoute { val appCurrency: AppCurrency, val shouldShowPortfolio: Boolean, val analyticsParams: AnalyticsParams? = null, + val preselectedSection: PreselectedTokenDetailsSection? = null, + val shouldOpenExchanges: Boolean = false, + val exchangesCount: Int? = null, ) : FeedEntryRoute { @Serializable @@ -23,7 +29,10 @@ sealed interface FeedEntryRoute { } @Serializable - data object MarketTokenList : FeedEntryRoute + data class MarketTokenList( + val preselectedOrder: PreselectedMarketsOrder? = null, + val preselectedInterval: PreselectedMarketsInterval? = null, + ) : FeedEntryRoute @Serializable data class NewsDetail(val articleId: Int, val preselectedArticlesId: List) : FeedEntryRoute diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/MarketsDeepLinkHandler.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/MarketsDeepLinkHandler.kt index 5909a4876a..4cd535a841 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/MarketsDeepLinkHandler.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/MarketsDeepLinkHandler.kt @@ -3,6 +3,6 @@ package com.tangem.features.feed.entry.deeplink interface MarketsDeepLinkHandler { interface Factory { - fun create(): MarketsDeepLinkHandler + fun create(params: Map): MarketsDeepLinkHandler } } \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/MarketsTokenExchangesDeepLinkHandler.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/MarketsTokenExchangesDeepLinkHandler.kt new file mode 100644 index 0000000000..1ebdae4226 --- /dev/null +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/MarketsTokenExchangesDeepLinkHandler.kt @@ -0,0 +1,10 @@ +package com.tangem.features.feed.entry.deeplink + +import kotlinx.coroutines.CoroutineScope + +interface MarketsTokenExchangesDeepLinkHandler { + + interface Factory { + fun create(coroutineScope: CoroutineScope, params: Map): MarketsTokenExchangesDeepLinkHandler + } +} \ 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 48d74bc9f4..74271e53a0 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 @@ -29,6 +29,9 @@ import com.tangem.features.feed.entry.components.FeedEntryComponent import com.tangem.features.feed.entry.components.FeedEntryRoute import com.tangem.features.feed.model.FeedEntryModel import com.tangem.features.feed.model.feed.FeedModelClickIntents +import com.tangem.domain.markets.PreselectedMarketsInterval +import com.tangem.domain.markets.PreselectedMarketsOrder +import com.tangem.features.feed.model.market.list.state.MarketsListUM import com.tangem.features.feed.model.market.list.state.SortByTypeUM import com.tangem.features.feed.ui.EntryContent import dagger.assisted.Assisted @@ -87,6 +90,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( route = FeedEntryChildFactory.Child.TokenList( params = DefaultMarketsTokenListComponent.Params( preselectedSortType = sortBy ?: SortByTypeUM.Rating, + preselectedInterval = MarketsListUM.TrendInterval.H24, shouldAlwaysShowSearchBar = sortBy == null, ), ), @@ -231,11 +235,15 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( paginationConfig = null, ) }, + preselectedSection = entryRoute.preselectedSection, + shouldOpenExchanges = entryRoute.shouldOpenExchanges, + exchangesCount = entryRoute.exchangesCount, ), ) - FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList( + is FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList( DefaultMarketsTokenListComponent.Params( - preselectedSortType = SortByTypeUM.Rating, + preselectedSortType = mapOrderToSortType(entryRoute.preselectedOrder), + preselectedInterval = mapIntervalToTrendInterval(entryRoute.preselectedInterval), shouldAlwaysShowSearchBar = false, ), ) @@ -265,4 +273,24 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( } } +private fun mapOrderToSortType(order: PreselectedMarketsOrder?): SortByTypeUM { + return when (order) { + PreselectedMarketsOrder.Rating -> SortByTypeUM.Rating + PreselectedMarketsOrder.Trending -> SortByTypeUM.Trending + PreselectedMarketsOrder.Buyers -> SortByTypeUM.ExperiencedBuyers + PreselectedMarketsOrder.Gainers -> SortByTypeUM.TopGainers + PreselectedMarketsOrder.Losers -> SortByTypeUM.TopLosers + null -> SortByTypeUM.Rating + } +} + +private fun mapIntervalToTrendInterval(interval: PreselectedMarketsInterval?): MarketsListUM.TrendInterval { + return when (interval) { + PreselectedMarketsInterval.H24 -> MarketsListUM.TrendInterval.H24 + PreselectedMarketsInterval.W1 -> MarketsListUM.TrendInterval.D7 + PreselectedMarketsInterval.D30 -> MarketsListUM.TrendInterval.M1 + null -> MarketsListUM.TrendInterval.H24 + } +} + internal interface FeedEntryClickIntents : FeedModelClickIntents \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt index a9c1b09117..c4ac5551aa 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.res.vectorResource import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network +import com.tangem.domain.markets.PreselectedTokenDetailsSection import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child @@ -228,6 +229,9 @@ internal class DefaultMarketsTokenDetailsComponent( val analyticsParams: AnalyticsParams?, val onBackClicked: () -> Unit, val onArticleClick: (articleId: Int, preselectedArticlesId: List) -> Unit, + val preselectedSection: PreselectedTokenDetailsSection? = null, + val shouldOpenExchanges: Boolean = false, + val exchangesCount: Int? = null, ) @Serializable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt index 2d69602f84..29e359843b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt @@ -29,6 +29,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.feed.model.market.list.MarketsListModel +import com.tangem.features.feed.model.market.list.state.MarketsListUM import com.tangem.features.feed.model.market.list.state.SortByTypeUM import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.market.list.MarketsList @@ -127,6 +128,7 @@ internal class DefaultMarketsTokenListComponent( @Serializable data class Params( val preselectedSortType: SortByTypeUM, + val preselectedInterval: MarketsListUM.TrendInterval, val shouldAlwaysShowSearchBar: Boolean, ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsDeepLinkHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsDeepLinkHandler.kt index 2e81775f75..b2d72c2863 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsDeepLinkHandler.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsDeepLinkHandler.kt @@ -2,20 +2,31 @@ 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.INTERVAL_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.ORDER_KEY +import com.tangem.domain.markets.PreselectedMarketsInterval +import com.tangem.domain.markets.PreselectedMarketsOrder import com.tangem.features.feed.entry.deeplink.MarketsDeepLinkHandler +import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject internal class DefaultMarketsDeepLinkHandler @AssistedInject constructor( + @Assisted private val queryParams: Map, appRouter: AppRouter, ) : MarketsDeepLinkHandler { init { - appRouter.push(AppRoute.Markets) + appRouter.push( + AppRoute.Markets( + preselectedOrder = PreselectedMarketsOrder.parse(queryParams[ORDER_KEY]), + preselectedInterval = PreselectedMarketsInterval.parse(queryParams[INTERVAL_KEY]), + ), + ) } @AssistedFactory interface Factory : MarketsDeepLinkHandler.Factory { - override fun create(): DefaultMarketsDeepLinkHandler + override fun create(params: Map): DefaultMarketsDeepLinkHandler } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt index 89c501508c..89cc9b9877 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt @@ -2,7 +2,9 @@ package com.tangem.features.feed.deeplink import arrow.core.getOrElse import com.tangem.common.routing.AppRoute +import com.tangem.domain.markets.PreselectedTokenDetailsSection import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.deeplink.DeeplinkConst.SECTION_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -11,12 +13,12 @@ import com.tangem.domain.markets.GetTokenMarketInfoUseCase import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.feed.entry.deeplink.MarketsTokenDetailDeepLinkHandler +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 -import com.tangem.utils.logging.TangemLogger internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject constructor( @Assisted private val scope: CoroutineScope, @@ -32,8 +34,15 @@ internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject construc private fun handleDeepLink() { val tokenId = queryParams[TOKEN_ID_KEY] + val section = PreselectedTokenDetailsSection.parse(queryParams[SECTION_KEY]) - val rawTokenId = CryptoCurrency.RawID(tokenId.orEmpty()) + if (tokenId.isNullOrEmpty()) { + TangemLogger.e("Markets token details deeplink does not contain token_id") + appRouter.push(AppRoute.Markets()) + return + } + + val rawTokenId = CryptoCurrency.RawID(tokenId) scope.launch { val appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { @@ -65,6 +74,7 @@ internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject construc appCurrency = appCurrency, shouldShowPortfolio = true, analyticsParams = null, + preselectedSection = section, ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenExchangesDeepLinkHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenExchangesDeepLinkHandler.kt new file mode 100644 index 0000000000..4fbd77c4ba --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenExchangesDeepLinkHandler.kt @@ -0,0 +1,88 @@ +package com.tangem.features.feed.deeplink + +import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY +import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.GetTokenMarketInfoUseCase +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.feed.entry.deeplink.MarketsTokenExchangesDeepLinkHandler +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 DefaultMarketsTokenExchangesDeepLinkHandler @AssistedInject constructor( + @Assisted private val scope: CoroutineScope, + @Assisted private val queryParams: Map, + private val appRouter: AppRouter, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, +) : MarketsTokenExchangesDeepLinkHandler { + + init { + handleDeepLink() + } + + private fun handleDeepLink() { + val tokenId = queryParams[TOKEN_ID_KEY] + + if (tokenId.isNullOrEmpty()) { + TangemLogger.e("Markets token exchanges deeplink does not contain token_id") + appRouter.push(AppRoute.Markets()) + return + } + + val rawTokenId = CryptoCurrency.RawID(tokenId) + + scope.launch { + val appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { + AppCurrency.Default + } + val tokenInfo = getTokenMarketInfoUseCase( + appCurrency = appCurrency, + tokenId = rawTokenId, + tokenSymbol = "", + ).getOrElse { + TangemLogger.e("Failed to get market token info for exchanges deeplink") + appRouter.push(AppRoute.Markets()) + return@launch + } + + appRouter.push( + AppRoute.MarketsTokenDetails( + token = TokenMarketParams( + id = rawTokenId, + name = tokenInfo.name, + symbol = tokenInfo.symbol, + tokenQuotes = TokenMarketParams.Quotes( + currentPrice = tokenInfo.quotes.currentPrice, + h24Percent = tokenInfo.quotes.h24ChangePercent, + weekPercent = tokenInfo.quotes.weekChangePercent, + monthPercent = tokenInfo.quotes.monthChangePercent, + ), + imageUrl = getTokenIconUrlFromDefaultHost(rawTokenId), + ), + appCurrency = appCurrency, + shouldShowPortfolio = true, + shouldOpenExchanges = true, + exchangesCount = tokenInfo.exchangesAmount, + ), + ) + } + } + + @AssistedFactory + interface Factory : MarketsTokenExchangesDeepLinkHandler.Factory { + override fun create( + coroutineScope: CoroutineScope, + queryParams: Map, + ): DefaultMarketsTokenExchangesDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/MarketsDeepLinkModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/MarketsDeepLinkModule.kt index def0bf84fb..1e1e90cc24 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/MarketsDeepLinkModule.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/MarketsDeepLinkModule.kt @@ -2,8 +2,10 @@ package com.tangem.features.feed.deeplink.di import com.tangem.features.feed.deeplink.DefaultMarketsDeepLinkHandler import com.tangem.features.feed.deeplink.DefaultMarketsTokenDetailDeepLinkHandler +import com.tangem.features.feed.deeplink.DefaultMarketsTokenExchangesDeepLinkHandler 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 dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -23,4 +25,10 @@ internal interface MarketsDeepLinkModule { fun bindMarketsTokenDetailDeepLinkHandlerFactory( impl: DefaultMarketsTokenDetailDeepLinkHandler.Factory, ): MarketsTokenDetailDeepLinkHandler.Factory + + @Binds + @Singleton + fun bindMarketsTokenExchangesDeepLinkHandlerFactory( + impl: DefaultMarketsTokenExchangesDeepLinkHandler.Factory, + ): MarketsTokenExchangesDeepLinkHandler.Factory } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt index 05f213308c..e412504e4d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt @@ -5,6 +5,7 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.TangemSiteShareUrlBuilder +import com.tangem.domain.markets.PreselectedTokenDetailsSection import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.common.ui.charts.state.sorted @@ -21,6 +22,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.format.bigdecimal.fiat @@ -95,6 +97,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( private val quotesJob = JobHolder() private var userCountry: UserCountry? = null + private var isScrollToSectionHandled = false private val params = paramsContainer.require() private val analyticsEventBuilder = MarketDetailsAnalyticsEvent.EventBuilder(token = params.token) @@ -297,6 +300,15 @@ internal class MarketsTokenDetailsModel @Inject constructor( initialLoad() loadRelatedNews() + + if (params.shouldOpenExchanges) { + modelScope.launch { + val exchangesCount = params.exchangesCount + ?: currentTokenInfo.value?.exchangesAmount + ?: 0 + onListedOnClick(exchangesCount) + } + } } private fun initialLoad() { @@ -519,6 +531,14 @@ internal class MarketsTokenDetailsModel @Inject constructor( description = descriptionConverter.convert(newInfo), infoBlocks = infoConverter.convert(newInfo), ), + scrollToSection = if (!isScrollToSectionHandled) { + mapSectionToKey(params.preselectedSection)?.let { key -> + isScrollToSectionHandled = true + triggeredEvent(data = key, onConsume = ::consumeScrollToSection) + } ?: consumedEvent() + } else { + marketsTokenDetailsUM.scrollToSection + }, ) } @@ -755,6 +775,17 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } + private fun mapSectionToKey(section: PreselectedTokenDetailsSection?): String? { + return when (section) { + PreselectedTokenDetailsSection.News -> MarketsTokenDetailsUM.RelatedNews.SECTION_KEY + null -> null + } + } + + private fun consumeScrollToSection() { + state.update { it.copy(scrollToSection = consumedEvent()) } + } + private companion object { const val QUOTES_UPDATE_INTERVAL_MILLIS = 60000L const val RELATED_NEWS_LIMIT = 10 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/MarketsListModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/MarketsListModel.kt index 11a933c2f1..79c0d6a2db 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/MarketsListModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/MarketsListModel.kt @@ -67,6 +67,7 @@ internal class MarketsListModel @Inject constructor( onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens()) }, shouldAlwaysShowSearchBar = Provider { modelParams.params.shouldAlwaysShowSearchBar }, preselectedSortType = Provider { modelParams.params.preselectedSortType }, + preselectedInterval = Provider { modelParams.params.preselectedInterval }, onBackClick = modelParams.clickIntents.onBackClicked, analyticsEventHandler = analyticsEventHandler, onSearchBarClick = modelParams.clickIntents.onSearchClicked, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListUMStateManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListUMStateManager.kt index fe1b3b15a1..473c06b89b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListUMStateManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListUMStateManager.kt @@ -27,6 +27,7 @@ internal class MarketsListUMStateManager( private val shouldAlwaysShowSearchBar: Provider, private val currentVisibleIds: Provider>, private val preselectedSortType: Provider, + private val preselectedInterval: Provider, private val onLoadMoreUiItems: () -> Unit, private val visibleItemsChanged: (itemsKeys: List) -> Unit, private val onRetryButtonClicked: () -> Unit, @@ -229,7 +230,7 @@ internal class MarketsListUMStateManager( shouldAlwaysShowSearchBar = shouldAlwaysShowSearchBar(), ), selectedSortBy = preselectedSortType(), - selectedInterval = MarketsListUM.TrendInterval.H24, + selectedInterval = preselectedInterval(), onIntervalClick = { selectedInterval = it }, onSortByButtonClick = { isSortByBottomSheetShown = true }, sortByBottomSheet = TangemBottomSheetConfig( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt index 195e45cf2f..79330be97f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt @@ -106,6 +106,13 @@ private fun Content( lazyListState = lazyListState, onShouldShowPriceSubtitleChange = state.onShouldShowPriceSubtitleChange, ) + EventEffect(state.scrollToSection) { targetKey -> + val targetIndex = lazyListState.layoutInfo.visibleItemsInfo + .firstOrNull { it.key == targetKey }?.index + if (targetIndex != null) { + lazyListState.animateScrollToItem(targetIndex) + } + } var bottomSpacing by remember { mutableStateOf(0.dp) } Box( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt index de92450ef8..b1d372de32 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt @@ -76,6 +76,8 @@ private fun LazyListScope.tokenMarketDetailsBodyV1( if (relatedNews.articles.isNotEmpty()) { relatedNews(relatedNews) + } else { + sectionStub(RelatedNews.SECTION_KEY) } aboutCoinHeader() @@ -91,6 +93,11 @@ private fun LazyListScope.tokenMarketDetailsBodyV1( } } +// Empty item with a key so that deeplink scroll-to-section can target it before the real content is composed +private fun LazyListScope.sectionStub(key: String) { + item(key) { } +} + @Suppress("CanBeNonNullable") private fun LazyListScope.tokenMarketDetailsBodyV2(state: MarketsTokenDetailsUM.Body, relatedNews: RelatedNews) { when (state) { @@ -244,6 +251,8 @@ internal fun LazyListScope.infoBlocksListV2(state: MarketsTokenDetailsUM.Informa if (relatedNews.articles.isNotEmpty()) { relatedNews(relatedNews) + } else { + sectionStub(RelatedNews.SECTION_KEY) } if (state.securityScore != null) { @@ -324,7 +333,7 @@ private fun LazyListScope.loadingInfoBlocksV2() { } private fun LazyListScope.relatedNews(relatedNews: RelatedNews) { - item("related-news") { + item(RelatedNews.SECTION_KEY) { Column( modifier = Modifier .fillMaxWidth() diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt index 2cd7078d51..cb53a7dd7e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt @@ -5,6 +5,7 @@ import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.markets.PriceChangeInterval import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM @@ -30,6 +31,7 @@ internal data class MarketsTokenDetailsUM( val onShouldShowPriceSubtitleChange: (Boolean) -> Unit, val relatedNews: RelatedNews, val onShareClick: () -> Unit, + val scrollToSection: StateEvent = consumedEvent(), ) { data class ChartState( @@ -80,5 +82,9 @@ internal data class MarketsTokenDetailsUM( val onArticledClicked: (id: Int) -> Unit, val onFirstVisible: () -> Unit, val onScroll: () -> Unit, - ) + ) { + companion object { + const val SECTION_KEY = "related-news" + } + } } \ No newline at end of file