From fc73f08b5e6044133b200d4bb2676d04e769a4f1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 May 2025 19:28:33 +0500 Subject: [PATCH 1/2] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 9 + .../com/tangem/common/routing/AppRoute.kt | 3 + .../tokenlist/MarketsTokenListComponent.kt | 23 ++ features/markets/impl/build.gradle.kts | 3 + .../impl/DefaultMarketsEntryComponent.kt | 48 +--- .../entry/impl/MarketTokenDetailsRouter.kt | 25 -- .../entry/impl/MarketsEntryChildFactory.kt | 16 +- .../entry/impl/ui/EntryBottomSheetContent.kt | 2 +- .../api/MarketsTokenListComponent.kt | 29 --- .../impl/DefaultMarketsTokenListComponent.kt | 68 ++++- .../tokenlist/impl/di/ComponentModule.kt | 2 +- .../markets/tokenlist/impl/ui/MarketsList.kt | 240 +++++++++++------- 12 files changed, 261 insertions(+), 207 deletions(-) create mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketTokenDetailsRouter.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt 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 238574faa4..1c3e20f554 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 @@ -12,6 +12,7 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.markets.details.MarketsTokenDetailsComponent +import com.tangem.features.markets.tokenlist.MarketsTokenListComponent import com.tangem.features.nft.component.NFTComponent import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent import com.tangem.features.onramp.component.* @@ -47,6 +48,7 @@ internal class ChildFactory @Inject constructor( private val disclaimerComponentFactory: DisclaimerComponent.Factory, private val manageTokensComponentFactory: ManageTokensComponent.Factory, private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, + private val marketsTokenListComponentFactory: MarketsTokenListComponent.Factory, private val onrampComponentFactory: OnrampComponent.Factory, private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory, private val buyCryptoComponentFactory: BuyCryptoComponent.Factory, @@ -389,6 +391,13 @@ internal class ChildFactory @Inject constructor( componentFactory = nftSendComponentFactory, ) } + is AppRoute.Markets -> { + createComponentChild( + context = context, + params = Unit, + componentFactory = marketsTokenListComponentFactory, + ) + } } } } \ No newline at end of file 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 a8361504f7..b9629af57a 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 @@ -182,6 +182,9 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, ) : AppRoute(path = "/wallet_settings/${userWalletId.stringValue}") + @Serializable + data object Markets : AppRoute(path = "/markets") + @Serializable data class MarketsTokenDetails( val token: TokenMarketParams, diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt new file mode 100644 index 0000000000..670816199f --- /dev/null +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt @@ -0,0 +1,23 @@ +package com.tangem.features.markets.tokenlist + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.markets.entry.BottomSheetState + +@Stable +interface MarketsTokenListComponent : ComposableContentComponent { + + @Composable + fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index fded1c34bc..6609be68ac 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -17,6 +17,9 @@ dependencies { api(projects.features.onramp.api) implementation(projects.core.navigation) + /* Data */ + implementation(projects.data.common) + /* Domain */ implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt index d2a5ed9d0b..da862ba234 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt @@ -5,20 +5,15 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp -import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.decompose.router.stack.pushNew +import com.arkivanov.decompose.router.stack.popWhile import com.arkivanov.decompose.value.Value import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.navigation.Router -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarket -import com.tangem.domain.markets.toSerializableParam -import com.tangem.features.markets.details.MarketsTokenDetailsComponent +import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child @@ -35,7 +30,12 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor( private val stackNavigation = StackNavigation() - val stack: Value> = childStack( + private val innerRouter = InnerRouter( + stackNavigation = stackNavigation, + popCallback = { onChildBack() }, + ) + + private val stack: Value> = childStack( key = "main", source = stackNavigation, serializer = Child.serializer(), @@ -46,9 +46,8 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor( child = configuration, appComponentContext = childByContext( componentContext = factoryContext, - router = createRouter(configuration), + router = innerRouter, ), - onTokenSelected = ::marketsListTokenSelected, ) }, ) @@ -68,32 +67,9 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor( ) } - @OptIn(ExperimentalDecomposeApi::class) - private fun marketsListTokenSelected(token: TokenMarket, appCurrency: AppCurrency) { - stackNavigation.pushNew( - configuration = Child.TokenDetails( - params = MarketsTokenDetailsComponent.Params( - token = token.toSerializableParam(), - appCurrency = appCurrency, - showPortfolio = true, - analyticsParams = MarketsTokenDetailsComponent.AnalyticsParams( - blockchain = null, - source = "Market", - ), - ), - ), - ) - } - - private fun AppComponentContext.createRouter(child: Child): Router { - return when (child) { - is Child.TokenDetails -> { - MarketTokenDetailsRouter( - contextRouter = this.router, - stackNavigation = stackNavigation, - ) - } - else -> this.router + private fun onChildBack() { + if (stack.value.active.configuration !is Child.TokenList) { + stackNavigation.popWhile { it != Child.TokenList } } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketTokenDetailsRouter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketTokenDetailsRouter.kt deleted file mode 100644 index fb1d1b1eff..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketTokenDetailsRouter.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.features.markets.entry.impl - -import com.arkivanov.decompose.router.stack.StackNavigation -import com.arkivanov.decompose.router.stack.popWhile -import com.tangem.core.decompose.navigation.Route -import com.tangem.core.decompose.navigation.Router -import kotlin.reflect.KClass - -internal class MarketTokenDetailsRouter( - private val contextRouter: Router, - private val stackNavigation: StackNavigation, -) : Router by contextRouter { - - override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { - stackNavigation.popWhile({ it != MarketsEntryChildFactory.Child.TokenList }, onComplete) - } - - override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { - /** Not allowed */ - } - - override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { - /** Not allowed */ - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt index 31e6f5e1e0..9a11de132d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt @@ -2,11 +2,9 @@ package com.tangem.features.markets.entry.impl import androidx.compose.runtime.Immutable import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarket +import com.tangem.core.decompose.navigation.Route import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child -import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent +import com.tangem.features.markets.tokenlist.MarketsTokenListComponent import kotlinx.serialization.Serializable import javax.inject.Inject @@ -17,7 +15,7 @@ internal class MarketsEntryChildFactory @Inject constructor( @Serializable @Immutable - sealed interface Child { + sealed interface Child : Route { @Serializable @Immutable @@ -28,11 +26,7 @@ internal class MarketsEntryChildFactory @Inject constructor( data class TokenDetails(val params: MarketsTokenDetailsComponent.Params) : Child } - fun createChild( - child: Child, - appComponentContext: AppComponentContext, - onTokenSelected: (TokenMarket, AppCurrency) -> Unit, - ): Any { + fun createChild(child: Child, appComponentContext: AppComponentContext): Any { return when (child) { is Child.TokenDetails -> { tokenDetailsComponentFactory.create( @@ -43,7 +37,7 @@ internal class MarketsEntryChildFactory @Inject constructor( is Child.TokenList -> { tokenListComponentFactory.create( context = appComponentContext, - onTokenSelected = onTokenSelected, + params = Unit, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt index 4adeb8b8c5..5d6ff14c2a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt @@ -20,7 +20,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory -import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent +import com.tangem.features.markets.tokenlist.MarketsTokenListComponent @Composable internal fun EntryBottomSheetContent( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt deleted file mode 100644 index 64b0f12b74..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.features.markets.tokenlist.api - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarket -import com.tangem.features.markets.entry.BottomSheetState - -@Stable -interface MarketsTokenListComponent { - - @Composable - fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) - - interface Factory { - fun create( - context: AppComponentContext, - onTokenSelected: (TokenMarket, AppCurrency) -> Unit, - ): MarketsTokenListComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt index a04a5f8b63..16ecb9a9c7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt @@ -1,34 +1,58 @@ package com.tangem.features.markets.tokenlist.impl -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRoute.MarketsTokenDetails.AnalyticsParams import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarket +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero +import com.tangem.domain.markets.toSerializableParam import com.tangem.features.markets.entry.BottomSheetState -import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent +import com.tangem.features.markets.tokenlist.MarketsTokenListComponent import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel import com.tangem.features.markets.tokenlist.impl.ui.MarketsList +import com.tangem.features.markets.tokenlist.impl.ui.MarketsListWithBack import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +@Suppress("UnusedPrivateMember") class DefaultMarketsTokenListComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - @Assisted private val onTokenSelected: (TokenMarket, AppCurrency) -> Unit, + @Assisted params: Unit, ) : AppComponentContext by appComponentContext, MarketsTokenListComponent { private val model: MarketsListModel = getOrCreateModel() init { model.tokenSelected - .onEach { onTokenSelected(it.first, it.second) } + .onEach { (token, appCurrency) -> + router.push( + AppRoute.MarketsTokenDetails( + token = token.toSerializableParam(), + appCurrency = appCurrency, + showPortfolio = true, + analyticsParams = AnalyticsParams( + blockchain = null, + source = "Market", + ), + ), + ) + } .launchIn(componentScope) } @@ -60,11 +84,35 @@ class DefaultMarketsTokenListComponent @AssistedInject constructor( ) } + @Composable + override fun Content(modifier: Modifier) { + LifecycleStartEffect(Unit) { + model.isVisibleOnScreen.value = true + onStopOrDispose { + model.isVisibleOnScreen.value = false + } + } + + val state by model.state.collectAsStateWithLifecycle() + + Scaffold( + contentWindowInsets = WindowInsetsZero, + containerColor = TangemTheme.colors.background.primary, + ) { + MarketsListWithBack( + modifier = Modifier + .statusBarsPadding() + .imePadding() + .padding(it), + state = state, + bottomSheetState = BottomSheetState.EXPANDED, + onBackClick = router::pop, + ) + } + } + @AssistedFactory interface Factory : MarketsTokenListComponent.Factory { - override fun create( - context: AppComponentContext, - onTokenSelected: (TokenMarket, AppCurrency) -> Unit, - ): DefaultMarketsTokenListComponent + override fun create(context: AppComponentContext, params: Unit): DefaultMarketsTokenListComponent } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/di/ComponentModule.kt index bb44e19164..4befeceeea 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/di/ComponentModule.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/di/ComponentModule.kt @@ -1,6 +1,6 @@ package com.tangem.features.markets.tokenlist.impl.di -import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent +import com.tangem.features.markets.tokenlist.MarketsTokenListComponent import com.tangem.features.markets.tokenlist.impl.DefaultMarketsTokenListComponent import dagger.Binds import dagger.Module diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index a0f8508f89..041df48f96 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -3,22 +3,29 @@ package com.tangem.features.markets.tokenlist.impl.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.text.buildAnnotatedString import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH8 @@ -62,26 +69,8 @@ internal fun MarketsList( bottomSheetState: BottomSheetState, modifier: Modifier = Modifier, ) { - Content( - modifier = modifier, - state = state, - onHeaderSizeChange = onHeaderSizeChange, - ) - MarketsListSortByBottomSheet(config = state.sortByBottomSheet) - KeyboardEvents( - isSortByBottomSheetShown = state.sortByBottomSheet.isShown, - bottomSheetState = bottomSheetState, - ) -} - -@Suppress("LongMethod") -@Composable -private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) { val density = LocalDensity.current val background = LocalMainBottomSheetColor.current.value - val strokeColor = TangemTheme.colors.stroke.primary - val scrolledState = remember { mutableStateOf(false) } - Column( modifier = modifier .fillMaxSize() @@ -106,85 +95,148 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi .padding(bottom = 4.dp), state = state.searchBar, ) - Column(Modifier.padding(horizontal = TangemTheme.dimens.size16)) { - AnimatedVisibility( - visible = scrolledState.value.not(), - ) { - Column { - SpacerH8() - Title(isInSearchMode = state.isInSearchMode) - SpacerH12() - } - } + Content(state = state) + } + MarketsListSortByBottomSheet(config = state.sortByBottomSheet) + KeyboardEvents( + isSortByBottomSheetShown = state.sortByBottomSheet.isShown, + bottomSheetState = bottomSheetState, + ) +} + +@Composable +internal fun MarketsListWithBack( + state: MarketsListUM, + bottomSheetState: BottomSheetState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val background = LocalMainBottomSheetColor.current.value + Column( + modifier = modifier + .fillMaxSize() + .imePadding() + .drawBehind { drawRect(background) }, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_close_24), + ), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + modifier = Modifier + .padding(16.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = false), + onClick = onBackClick, + ), + ) + SearchBar( + modifier = Modifier + .drawBehind { drawRect(background) } + .padding( + end = 16.dp, + ), + state = state.searchBar, + ) + } + Content(state = state) + } + MarketsListSortByBottomSheet(config = state.sortByBottomSheet) + KeyboardEvents( + isSortByBottomSheetShown = state.sortByBottomSheet.isShown, + bottomSheetState = bottomSheetState, + ) +} + +@Suppress("LongMethod") +@Composable +private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modifier) { + val strokeColor = TangemTheme.colors.stroke.primary + val scrolledState = remember { mutableStateOf(false) } + + Column(modifier.padding(horizontal = TangemTheme.dimens.size16)) { + AnimatedVisibility( + visible = scrolledState.value.not(), + ) { Column { - AnimatedVisibility(state.isInSearchMode.not()) { - Options( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - sortByTypeUM = state.selectedSortBy, - trendInterval = state.selectedInterval, - onIntervalClick = state.onIntervalClick, - onSortByClick = state.onSortByButtonClick, - ) - } - - AnimatedVisibility( - state.isInSearchMode.not() && - state.stakingNotificationMaxApy != null && - state.selectedSortBy != SortByTypeUM.Staking, - ) { - val showMore = stringResourceSafe(R.string.common_show_more) - val description = stringResourceSafe( - R.string.markets_staking_banner_description_placeholder, - showMore, - ) - - val clickableDescription = buildAnnotatedString { - append(description.substringBefore(showMore)) - - pushStringAnnotation(SHOW_MORE_KEY, "") - appendColored(showMore, TangemTheme.colors.text.accent) - pop() - } - - StakingInMarketsPromoNotification( - config = NotificationConfig( - iconResId = R.drawable.img_staking_in_market_notification, - title = resourceReference( - R.string.markets_staking_banner_title, - wrappedList(state.stakingNotificationMaxApy.format { percent() }), - ), - subtitle = annotatedReference(clickableDescription), - onClick = state.onStakingNotificationClick, - onCloseClick = state.onStakingNotificationCloseClick, - ), - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - ) - } + SpacerH8() + Title(isInSearchMode = state.isInSearchMode) + SpacerH12() + } + } + Column { + AnimatedVisibility(state.isInSearchMode.not()) { + Options( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + sortByTypeUM = state.selectedSortBy, + trendInterval = state.selectedInterval, + onIntervalClick = state.onIntervalClick, + onSortByClick = state.onSortByButtonClick, + ) + } + + AnimatedVisibility( + state.isInSearchMode.not() && + state.stakingNotificationMaxApy != null && + state.selectedSortBy != SortByTypeUM.Staking, + ) { + val showMore = stringResourceSafe(R.string.common_show_more) + val description = stringResourceSafe( + R.string.markets_staking_banner_description_placeholder, + showMore, + ) + + val clickableDescription = buildAnnotatedString { + append(description.substringBefore(showMore)) + + pushStringAnnotation(SHOW_MORE_KEY, "") + appendColored(showMore, TangemTheme.colors.text.accent) + pop() + } + + StakingInMarketsPromoNotification( + config = NotificationConfig( + iconResId = R.drawable.img_staking_in_market_notification, + title = resourceReference( + R.string.markets_staking_banner_title, + wrappedList(state.stakingNotificationMaxApy.format { percent() }), + ), + subtitle = annotatedReference(clickableDescription), + onClick = state.onStakingNotificationClick, + onCloseClick = state.onStakingNotificationCloseClick, + ), + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + ) } } - val strokeWidth = TangemTheme.dimens.size0_5 - Box( - Modifier - .fillMaxWidth() - .height(strokeWidth) - .drawBehind { - // draw horizontal line - if (scrolledState.value) { - drawLine( - color = strokeColor, - start = Offset(0f, size.height), - end = Offset(size.width, size.height), - strokeWidth = strokeWidth.toPx(), - ) - } - }, - ) - ItemsList( - scrolledState = scrolledState, - isInSearchMode = state.isInSearchMode, - state = state.list, - ) } + val strokeWidth = TangemTheme.dimens.size0_5 + Box( + Modifier + .fillMaxWidth() + .height(strokeWidth) + .drawBehind { + // draw horizontal line + if (scrolledState.value) { + drawLine( + color = strokeColor, + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = strokeWidth.toPx(), + ) + } + }, + ) + ItemsList( + scrolledState = scrolledState, + isInSearchMode = state.isInSearchMode, + state = state.list, + ) } @Composable From c763f84aadc4b8616bda0f17a20beef8600424ba Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 May 2025 19:28:40 +0500 Subject: [PATCH 2/2] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 22 ++++++ .../tap/routing/utils/DeepLinkFactory.kt | 6 ++ .../tap/routing/utils/DeepLinkFactoryTest.kt | 30 +++++-- .../tangem/common/routing/DeepLinkRoute.kt | 8 ++ .../deeplink/MarketsDeepLinkHandler.kt | 8 ++ .../MarketsTokenDetailDeepLinkHandler.kt | 10 +++ .../deeplink/DefaultMarketsDeepLinkHandler.kt | 20 +++++ ...efaultMarketsTokenDetailDeepLinkHandler.kt | 79 +++++++++++++++++++ .../deeplink/di/MarketsDeepLinkModule.kt | 26 ++++++ 9 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/deeplink/MarketsDeepLinkHandler.kt create mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/deeplink/MarketsTokenDetailDeepLinkHandler.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsDeepLinkHandler.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/di/MarketsDeepLinkModule.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b61add5332..06a868e0a4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -186,6 +186,28 @@ android:host="staking" android:scheme="tangem" /> + + + + + + + + + + + + + + + + + + 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 00e016b292..f33be6d875 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 @@ -5,6 +5,8 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.DeepLinkRoute import com.tangem.common.routing.DeepLinkScheme import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler +import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler +import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler @@ -35,6 +37,8 @@ internal class DeepLinkFactory @Inject constructor( private val walletDeepLink: WalletDeepLinkHandler.Factory, private val tokenDetailsDeepLink: TokenDetailsDeepLinkHandler.Factory, private val stakingDeepLink: StakingDeepLinkHandler.Factory, + private val marketsDeepLink: MarketsDeepLinkHandler.Factory, + private val marketsTokenDetailDeepLink: MarketsTokenDetailDeepLinkHandler.Factory, ) { private val permittedAppRoute = MutableStateFlow(false) @@ -109,6 +113,8 @@ internal class DeepLinkFactory @Inject constructor( isFromOnNewIntent = isFromOnNewIntent, ) DeepLinkRoute.Staking.host -> stakingDeepLink.create(coroutineScope, queryParams) + DeepLinkRoute.Markets.host -> marketsDeepLink.create() + DeepLinkRoute.MarketTokenDetail.host -> marketsTokenDetailDeepLink.create(coroutineScope, queryParams) else -> { Timber.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 40a82fad7e..8078f58b05 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 @@ -3,6 +3,8 @@ package com.tangem.tap.routing.utils import android.net.Uri import com.tangem.common.routing.AppRoute import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler +import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler +import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler @@ -49,6 +51,12 @@ class DeepLinkFactoryTest { private val stakingDeepLinkFactory = mockk(relaxed = true) { every { create(any(), any()) } returns mockk() } + private val marketsDeepLinkFactory = mockk(relaxed = true) { + every { create() } returns mockk() + } + private val marketsTokenDetailDeepLinkFactory = mockk(relaxed = true) { + every { create(any(), any()) } returns mockk() + } private val mockedUri = mockk(relaxed = true) private val isFromOnNewIntent: Boolean = false @@ -65,6 +73,8 @@ class DeepLinkFactoryTest { walletDeepLinkFactory, tokenDetailsDeepLinkFactory, stakingDeepLinkFactory, + marketsDeepLinkFactory, + marketsTokenDetailDeepLinkFactory, ) @OptIn(ExperimentalCoroutinesApi::class) @@ -220,13 +230,13 @@ class DeepLinkFactoryTest { every { mockedUri.host } returns "staking" deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() - verify { - tokenDetailsDeepLinkFactory.create( - eq(testScope), - eq(mapOf("param" to "value")), - eq(isFromOnNewIntent), - ) - } + verify { stakingDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) } + + // Test Market Token Detail + every { mockedUri.host } returns "token_chart" + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) + advanceUntilIdle() + verify { marketsTokenDetailDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) } // Reset params every { mockedUri.queryParameterNames } returns emptySet() @@ -249,6 +259,12 @@ class DeepLinkFactoryTest { deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() verify { walletDeepLinkFactory.create() } + + // Test Markets + every { mockedUri.host } returns "markets" + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) + advanceUntilIdle() + verify { marketsDeepLinkFactory.create() } } @Test 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 3d7be7ec68..26a1ac0ce5 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 @@ -31,6 +31,14 @@ sealed class DeepLinkRoute { data object Staking : DeepLinkRoute() { override val host: String = "staking" } + + data object Markets : DeepLinkRoute() { + override val host: String = "markets" + } + + data object MarketTokenDetail : DeepLinkRoute() { + override val host: String = "token_chart" + } } enum class DeepLinkScheme(val scheme: String) { diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/deeplink/MarketsDeepLinkHandler.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/deeplink/MarketsDeepLinkHandler.kt new file mode 100644 index 0000000000..e88a78c729 --- /dev/null +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/deeplink/MarketsDeepLinkHandler.kt @@ -0,0 +1,8 @@ +package com.tangem.features.markets.deeplink + +interface MarketsDeepLinkHandler { + + interface Factory { + fun create(): MarketsDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/deeplink/MarketsTokenDetailDeepLinkHandler.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/deeplink/MarketsTokenDetailDeepLinkHandler.kt new file mode 100644 index 0000000000..45ff62203c --- /dev/null +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/deeplink/MarketsTokenDetailDeepLinkHandler.kt @@ -0,0 +1,10 @@ +package com.tangem.features.markets.deeplink + +import kotlinx.coroutines.CoroutineScope + +interface MarketsTokenDetailDeepLinkHandler { + + interface Factory { + fun create(coroutineScope: CoroutineScope, params: Map): MarketsTokenDetailDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsDeepLinkHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsDeepLinkHandler.kt new file mode 100644 index 0000000000..e620aaae47 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsDeepLinkHandler.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.deeplink + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultMarketsDeepLinkHandler @AssistedInject constructor( + appRouter: AppRouter, +) : MarketsDeepLinkHandler { + + init { + appRouter.push(AppRoute.Markets) + } + + @AssistedFactory + interface Factory : MarketsDeepLinkHandler.Factory { + override fun create(): DefaultMarketsDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt new file mode 100644 index 0000000000..5007ac70ab --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt @@ -0,0 +1,79 @@ +package com.tangem.features.markets.deeplink + +import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +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 dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject constructor( + @Assisted scope: CoroutineScope, + @Assisted queryParams: Map, + appRouter: AppRouter, + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, +) : MarketsTokenDetailDeepLinkHandler { + + init { + val tokenId = queryParams[TOKEN_ID_KEY] + + val rawTokenId = CryptoCurrency.RawID(tokenId.orEmpty()) + + scope.launch { + val appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { + AppCurrency.Default + } + val tokenInfo = getTokenMarketInfoUseCase( + appCurrency = appCurrency, + tokenId = rawTokenId, + tokenSymbol = TOKEN_SYMBOL_KEY, + ).getOrElse { + Timber.e("Failed to get market token info") + 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, + showPortfolio = true, + analyticsParams = null, + ), + ) + } + } + + @AssistedFactory + interface Factory : MarketsTokenDetailDeepLinkHandler.Factory { + override fun create( + coroutineScope: CoroutineScope, + queryParams: Map, + ): DefaultMarketsTokenDetailDeepLinkHandler + } + + private companion object { + const val TOKEN_ID_KEY = "token_id" + const val TOKEN_SYMBOL_KEY = "token_symbol" + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/di/MarketsDeepLinkModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/di/MarketsDeepLinkModule.kt new file mode 100644 index 0000000000..168ea58a46 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/di/MarketsDeepLinkModule.kt @@ -0,0 +1,26 @@ +package com.tangem.features.markets.deeplink.di + +import com.tangem.features.markets.deeplink.DefaultMarketsDeepLinkHandler +import com.tangem.features.markets.deeplink.DefaultMarketsTokenDetailDeepLinkHandler +import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler +import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface MarketsDeepLinkModule { + + @Binds + @Singleton + fun bindMarketsDeepLinkHandlerFactory(impl: DefaultMarketsDeepLinkHandler.Factory): MarketsDeepLinkHandler.Factory + + @Binds + @Singleton + fun bindMarketsTokenDetailDeepLinkHandlerFactory( + impl: DefaultMarketsTokenDetailDeepLinkHandler.Factory, + ): MarketsTokenDetailDeepLinkHandler.Factory +} \ No newline at end of file