Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-03 12:25:53 +03:00
commit 7c629b68d1
1179 changed files with 24718 additions and 16703 deletions

View file

@ -0,0 +1,8 @@
package com.tangem.features.markets.deeplink
interface MarketsDeepLinkHandler {
interface Factory {
fun create(): MarketsDeepLinkHandler
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.markets.deeplink
import kotlinx.coroutines.CoroutineScope
interface MarketsTokenDetailDeepLinkHandler {
interface Factory {
fun create(coroutineScope: CoroutineScope, params: Map<String, String>): MarketsTokenDetailDeepLinkHandler
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.features.markets.token.block
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import kotlinx.serialization.Serializable
@Stable

View file

@ -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<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
)
interface Factory : ComponentFactory<Unit, MarketsTokenListComponent>
}

View file

@ -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)

View file

@ -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
}
}

View file

@ -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<String, String>,
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<String, String>,
): DefaultMarketsTokenDetailDeepLinkHandler
}
private companion object {
const val TOKEN_ID_KEY = "token_id"
const val TOKEN_SYMBOL_KEY = "token_symbol"
}
}

View file

@ -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
}

View file

@ -13,7 +13,7 @@ import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params
import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent

View file

@ -1,117 +0,0 @@
package com.tangem.features.markets.details.impl.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.PreviewShimmerContainer
import com.tangem.features.markets.impl.R
import com.tangem.utils.StringsSigns
@Composable
internal fun Description(
description: TextReference,
hasFullDescription: Boolean,
onReadMoreClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (hasFullDescription) {
val text = buildAnnotatedString {
withStyle(SpanStyle(color = TangemTheme.colors.text.secondary)) {
append(description.resolveReference())
}
withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) {
append(
" " + stringResourceSafe(R.string.common_read_more).replace(
' ',
StringsSigns.NON_BREAKING_SPACE,
),
)
}
}
Text(
modifier = modifier
.clickable(
interactionSource = null,
indication = null,
onClick = onReadMoreClick,
),
text = text,
style = TangemTheme.typography.body2,
)
} else {
Text(
modifier = modifier,
text = description.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
@Composable
internal fun DescriptionPlaceholder(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
) {
TextShimmer(
modifier = Modifier.fillMaxWidth(),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
TextShimmer(
modifier = Modifier.fillMaxWidth(),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
TextShimmer(
modifier = Modifier.fillMaxWidth(fraction = 0.8f),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
}
}
@Preview
@Composable
private fun ContentPreview() {
TangemThemePreview {
Description(
description = stringReference(
"XRP (XRP) is a cryptocurrency launched in January 2009, where the first " +
"genesis block was mined on 9th January 2009",
),
hasFullDescription = true,
onReadMoreClick = {},
)
}
}
@Preview
@Composable
private fun PreviewPlaceholder() {
TangemThemePreview {
PreviewShimmerContainer(
actualContent = {
ContentPreview()
},
shimmerContent = {
DescriptionPlaceholder()
},
)
}
}

View file

@ -10,6 +10,8 @@ import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.items.DescriptionItem
import com.tangem.core.ui.components.items.DescriptionPlaceholder
internal fun LazyListScope.tokenMarketDetailsBody(
state: MarketsTokenDetailsUM.Body,
@ -69,7 +71,7 @@ private fun LazyListScope.error(state: MarketsTokenDetailsUM.Body.Error) {
private fun LazyListScope.description(description: MarketsTokenDetailsUM.Description) {
item("description") {
Description(
DescriptionItem(
modifier = Modifier.blockPaddings(),
description = description.shortDescription,
hasFullDescription = description.fullDescription != null,

View file

@ -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<Child>()
val stack: Value<ChildStack<Child, Any>> = childStack(
private val innerRouter = InnerRouter<Child>(
stackNavigation = stackNavigation,
popCallback = { onChildBack() },
)
private val stack: Value<ChildStack<Child, Any>> = 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 }
}
}

View file

@ -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<MarketsEntryChildFactory.Child>,
) : 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<out Route>, onComplete: (isSuccess: Boolean) -> Unit) {
/** Not allowed */
}
}

View file

@ -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,
)
}
}

View file

@ -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(

View file

@ -7,11 +7,11 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId

View file

@ -11,6 +11,7 @@ import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM
import com.tangem.features.markets.portfolio.impl.ui.state.WalletSelectorBSContentUM

View file

@ -21,9 +21,11 @@ import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.markets.SaveMarketTokensUseCase
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.markets.impl.R
@ -195,6 +197,7 @@ internal class MarketsPortfolioModel @Inject constructor(
modelScope.launch {
loadArtworksMutex.withLock {
wallets.forEach { wallet ->
wallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
if (!loadedArtworks.containsKey(wallet.walletId)) {
val artwork = getCardImageUseCase(
cardId = wallet.cardId,

View file

@ -6,6 +6,7 @@ import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState

View file

@ -14,10 +14,10 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.network.Network
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.markets.impl.R
@ -59,7 +59,8 @@ internal class TokenActionsHandler @AssistedInject constructor(
cryptoCurrencyData = cryptoCurrencyData,
),
)
if (handleDemoMode(action, cryptoCurrencyData.userWallet)) return
val userWallet = cryptoCurrencyData.userWallet
if (userWallet is UserWallet.Cold && handleDemoMode(action, userWallet)) return
when (action) {
TokenActionsBSContentUM.Action.Buy -> onBuyClick(cryptoCurrencyData)
@ -72,7 +73,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
}
}
private fun handleDemoMode(action: TokenActionsBSContentUM.Action, userWallet: UserWallet): Boolean {
private fun handleDemoMode(action: TokenActionsBSContentUM.Action, userWallet: UserWallet.Cold): Boolean {
val demoCard = isDemoCardUseCase.invoke(userWallet.cardId)
val needShowDemoWarning = demoCard && disabledActionsInDemoMode.contains(action)

View file

@ -4,6 +4,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.markets.impl.R
@ -25,7 +26,7 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider
val userWallet = UserWalletItemUM(
id = UserWalletId("1"),
name = stringReference("Wallet 1"),
information = stringReference("3 cards"),
information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")),
balance = UserWalletItemUM.Balance.Loading,
isEnabled = true,
endIcon = UserWalletItemUM.EndIcon.Arrow,

View file

@ -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<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
)
interface Factory {
fun create(
context: AppComponentContext,
onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
): MarketsTokenListComponent
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -11,12 +11,12 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.GetStakingNotificationMaxApyUseCase
import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.settings.usercountry.models.UserCountryError
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager

View file

@ -2,7 +2,7 @@ package com.tangem.features.markets.tokenlist.impl.model.statemanager
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.*
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.tokenlist.impl.model.converters.MarketsTokenItemConverter
import com.tangem.features.markets.tokenlist.impl.model.utils.logAction
import com.tangem.features.markets.tokenlist.impl.model.utils.logStatus

View file

@ -6,7 +6,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.state.*
import com.tangem.utils.Provider

View file

@ -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
@ -38,7 +45,7 @@ import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn
@ -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

View file

@ -18,7 +18,7 @@ import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
import kotlinx.coroutines.launch

View file

@ -1,11 +1,12 @@
@file:Suppress("MagicNumber")
package com.tangem.features.markets.tokenlist.impl.ui.preview
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import kotlinx.collections.immutable.persistentListOf

View file

@ -5,7 +5,7 @@ import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
@Immutable
data class MarketsListItemUM(

View file

@ -6,7 +6,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal