Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-07 16:50:31 +03:00
parent 24c7340558
commit 7a6ea23dd4
14 changed files with 816 additions and 59 deletions

View file

@ -5,6 +5,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.features.feed.components.earn.DefaultEarnComponent
import com.tangem.features.feed.components.feed.DefaultFeedComponent
@ -12,6 +13,7 @@ import com.tangem.features.feed.components.feed.DefaultFeedComponent.FeedParams
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
@ -22,12 +24,15 @@ import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import kotlinx.serialization.Serializable
import javax.inject.Inject
@Suppress("LongParameterList")
internal class FeedEntryChildFactory @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val portfolioComponentFactory: MarketsPortfolioComponent.Factory,
private val portfolioBlockComponentFactory: PortfolioBlockComponent.Factory,
private val addToPortfolioPreselectedDataComponent: AddToPortfolioPreselectedDataComponent.Factory,
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
private val newPromoBannersFeatureToggles: NewPromoBannersFeatureToggles,
private val designFeatureToggles: DesignFeatureToggles,
) {
@Serializable
@ -77,6 +82,8 @@ internal class FeedEntryChildFactory @Inject constructor(
params = child.params,
analyticsEventHandler = analyticsEventHandler,
portfolioComponentFactory = portfolioComponentFactory,
portfolioBlockComponentFactory = portfolioBlockComponentFactory,
designFeatureToggles = designFeatureToggles,
)
}
is Child.TokenList -> {

View file

@ -21,6 +21,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.haze.hazeEffectTangem
@ -35,6 +36,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel
import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent
import com.tangem.features.feed.model.market.details.state.TokenNetworksState
@ -49,7 +51,9 @@ internal class DefaultMarketsTokenDetailsComponent(
appComponentContext: AppComponentContext,
val params: Params,
analyticsEventHandler: AnalyticsEventHandler,
designFeatureToggles: DesignFeatureToggles,
portfolioComponentFactory: MarketsPortfolioComponent.Factory,
portfolioBlockComponentFactory: PortfolioBlockComponent.Factory,
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
// applying l2 compatibility
@ -61,25 +65,41 @@ internal class DefaultMarketsTokenDetailsComponent(
private val analyticsParams = params.analyticsParams
private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams)
private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.shouldShowPortfolio) {
portfolioComponentFactory.create(
context = child("my_portfolio"),
params = MarketsPortfolioComponent.Params(
updatedParams.token,
analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) },
),
)
} else {
null
}
private val portfolioComponent: MarketsPortfolioComponent? =
if (updatedParams.shouldShowPortfolio && !designFeatureToggles.isRedesignEnabled) {
portfolioComponentFactory.create(
context = child("my_portfolio"),
params = MarketsPortfolioComponent.Params(
updatedParams.token,
analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) },
),
)
} else {
null
}
private val portfolioBlockComponent: PortfolioBlockComponent? =
if (updatedParams.shouldShowPortfolio && designFeatureToggles.isRedesignEnabled) {
portfolioBlockComponentFactory.create(
context = child("portfolio_block"),
params = PortfolioBlockComponent.Params(updatedParams.token),
)
} else {
null
}
init {
componentScope.launch(dispatchers.default) {
model.networksState.collectLatest { state ->
when (state) {
is TokenNetworksState.NetworksAvailable -> portfolioComponent?.setTokenNetworks(state.networks)
TokenNetworksState.NoNetworksAvailable -> portfolioComponent?.setNoNetworksAvailable()
is TokenNetworksState.NetworksAvailable -> {
portfolioBlockComponent?.setTokenNetworks(state.networks)
portfolioComponent?.setTokenNetworks(state.networks)
}
TokenNetworksState.NoNetworksAvailable -> {
portfolioBlockComponent?.setNoNetworksAvailable()
portfolioComponent?.setNoNetworksAvailable()
}
else -> {}
}
}
@ -192,6 +212,11 @@ internal class DefaultMarketsTokenDetailsComponent(
component.Content(blockModifier)
}
},
portfolioFloatingBlock = portfolioBlockComponent?.let { component ->
{ blockModifier ->
component.Content(blockModifier)
}
},
)
}

View file

@ -7,6 +7,7 @@ import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import kotlinx.serialization.Serializable
@Deprecated("Will be removed in favor of PortfolioComponent with redesigned UI")
@Stable
interface MarketsPortfolioComponent : ComposableContentComponent {

View file

@ -0,0 +1,115 @@
package com.tangem.features.feed.components.market.details.portfolioblock
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.components.market.details.portfolioblock.model.PortfolioBlockModel
import com.tangem.features.feed.components.market.details.portfolioblock.model.PortfolioBlockRoute
import com.tangem.features.feed.components.market.details.portfolioblock.ui.PortfolioBlock
import com.tangem.features.feed.components.portfolio.PortfolioComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.serialization.Serializable
@Stable
internal class PortfolioBlockComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: Params,
) : ComposableContentComponent, AppComponentContext by context {
private val portfolioComponentFactory: PortfolioComponent.Factory = object : PortfolioComponent.Factory {
override fun create(context: AppComponentContext, params: PortfolioComponent.Params): PortfolioComponent {
TODO("STUB. Will be implemented")
}
}
@Serializable
data class Params(
val token: TokenMarketParams,
)
private val model: PortfolioBlockModel = getOrCreateModel(params)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = PortfolioBlockRoute.serializer(),
handleBackButton = true,
childFactory = ::createBottomSheetChild,
)
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
model.setTokenNetworks(networks)
}
fun setNoNetworksAvailable() {
model.setNoNetworksAvailable()
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
PortfolioBlock(modifier = modifier, state = state)
bottomSheet.child?.instance?.BottomSheet()
}
@Suppress("UnusedParameter")
private fun createBottomSheetChild(
config: PortfolioBlockRoute,
componentContext: ComponentContext,
): ComposableBottomSheetComponent {
val currencyId = model.cryptoCurrencyIdState.value ?: return ComposableBottomSheetComponent.EMPTY
val portfolioComponent = portfolioComponentFactory.create(
context = childByContext(componentContext),
params = PortfolioComponent.Params(id = currencyId),
)
return PortfolioBottomSheetWrapper(
portfolioComponent = portfolioComponent,
onDismiss = { model.bottomSheetNavigation.dismiss() },
)
}
private class PortfolioBottomSheetWrapper(
private val portfolioComponent: PortfolioComponent,
private val onDismiss: () -> Unit,
) : ComposableBottomSheetComponent {
override fun dismiss() = onDismiss()
@Composable
override fun BottomSheet() {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
),
) {
portfolioComponent.Content(modifier = Modifier)
}
}
}
@AssistedFactory
interface Factory {
fun create(context: AppComponentContext, params: Params): PortfolioBlockComponent
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.feed.components.market.details.portfolioblock.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.feed.components.market.details.portfolioblock.model.PortfolioBlockModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface PortfolioBlockModelModule {
@Binds
@IntoMap
@ClassKey(PortfolioBlockModel::class)
fun providePortfolioBlockModel(model: PortfolioBlockModel): Model
}

View file

@ -0,0 +1,188 @@
package com.tangem.features.feed.components.market.details.portfolioblock.model
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.SpanStyle
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
import com.tangem.common.getTotalFiatAmount
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.formatStyled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
import com.tangem.features.feed.components.market.details.portfolioblock.ui.state.PortfolioBlockUM
import com.tangem.features.feed.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import java.math.BigDecimal
import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class)
@Stable
@ModelScoped
internal class PortfolioBlockModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
private val allAccountSupplier: MultiAccountStatusListSupplier,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : Model() {
val state: StateFlow<PortfolioBlockUM>
field = MutableStateFlow<PortfolioBlockUM>(PortfolioBlockUM.Loading)
val bottomSheetNavigation: SlotNavigation<PortfolioBlockRoute> = SlotNavigation()
val cryptoCurrencyIdState: StateFlow<CryptoCurrency.ID?>
field = MutableStateFlow(null)
private val params = paramsContainer.require<PortfolioBlockComponent.Params>()
private val currencyRawId: CryptoCurrency.RawID = params.token.id
private val tokenIcon: CurrencyIconState = CurrencyIconState.CoinIcon(
url = params.token.imageUrl,
fallbackResId = R.drawable.ic_custom_token_44,
isGrayscale = false,
shouldShowCustomBadge = false,
)
private val availableNetworks = MutableSharedFlow<List<TokenMarketInfo.Network>>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
init {
combineData()
.onEach { state.value = it }
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
availableNetworks.tryEmit(networks)
}
fun setNoNetworksAvailable() {
availableNetworks.tryEmit(emptyList())
}
private fun combineData(): Flow<PortfolioBlockUM> {
return availableNetworks.transformLatest { networks ->
if (networks.isEmpty()) {
emit(PortfolioBlockUM.Hidden)
} else {
emitAll(portfolioFlow().distinctUntilChanged())
}
}.distinctUntilChanged()
}
private fun portfolioFlow(): Flow<PortfolioBlockUM> {
val portfolioDataFlow = allAccountSupplier().flatMapLatest { accountStatusLists ->
val walletFlows = accountStatusLists.map { accountStatusList ->
getUserWalletUseCase.invokeFlow(accountStatusList.userWalletId)
.mapNotNull { it.getOrNull() }
.map { wallet ->
WalletPortfolio(
userWallet = wallet,
currencies = accountStatusList.filterByRawId(),
)
}
}
if (walletFlows.isEmpty()) {
flowOf(emptyList())
} else {
combine(walletFlows) { it.toList() }
}
}.distinctUntilChanged()
val settingsFlow = combine(
getSelectedAppCurrencyUseCase.invokeOrDefault(),
getBalanceHidingSettingsUseCase.isBalanceHidden(),
) { appCurrency, isBalanceHidden ->
SettingsBox(appCurrency, isBalanceHidden)
}.distinctUntilChanged()
return combine(portfolioDataFlow, settingsFlow) { portfolios, settings ->
buildState(portfolios, settings)
}.distinctUntilChanged()
}
private fun AccountStatusList.filterByRawId(): List<CryptoCurrencyStatus> {
return accountStatuses.filterCryptoPortfolio().flatMap { accountStatus ->
accountStatus.tokenList.flattenCurrencies().filter { status ->
val statusRawId = status.currency.id.rawCurrencyId ?: return@filter false
getTokenIdIfL2Network(statusRawId.value) == currencyRawId.value
}
}
}
private fun buildState(portfolios: List<WalletPortfolio>, settings: SettingsBox): PortfolioBlockUM {
val allCurrencies = portfolios.flatMap { it.currencies }
val hasMultiCurrencyWallet = portfolios.any { it.userWallet.isMultiCurrency }
if (allCurrencies.isEmpty()) {
return if (hasMultiCurrencyWallet) {
PortfolioBlockUM.AddToken(
tokenIcon = tokenIcon,
onClick = { bottomSheetNavigation.activate(PortfolioBlockRoute) },
)
} else {
PortfolioBlockUM.Hidden
}
}
cryptoCurrencyIdState.update {
it ?: allCurrencies.first().currency.id
}
val totalFiat = allCurrencies.mapNotNull { it.getTotalFiatAmount() }
.fold(BigDecimal.ZERO, BigDecimal::add)
val formattedBalance = totalFiat.formatStyled {
fiat(
fiatCurrencyCode = settings.appCurrency.code,
fiatCurrencySymbol = settings.appCurrency.symbol,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
)
}
return PortfolioBlockUM.Content(
totalBalance = formattedBalance,
tokensInPortfolioCount = allCurrencies.size,
tokenIcon = tokenIcon,
tokenName = allCurrencies.first().currency.name,
tokenSymbol = allCurrencies.first().currency.symbol,
isBalanceHidden = settings.isBalanceHidden,
onClick = { bottomSheetNavigation.activate(PortfolioBlockRoute) },
)
}
}
private data class WalletPortfolio(
val userWallet: UserWallet,
val currencies: List<CryptoCurrencyStatus>,
)
private data class SettingsBox(
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
)

View file

@ -0,0 +1,7 @@
package com.tangem.features.feed.components.market.details.portfolioblock.model
import com.tangem.core.decompose.navigation.Route
import kotlinx.serialization.Serializable
@Serializable
internal data object PortfolioBlockRoute : Route

View file

@ -0,0 +1,266 @@
package com.tangem.features.feed.components.market.details.portfolioblock.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterExitState
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.ds.button.*
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.formatAnnotatedWithBoldColor
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.feed.components.market.details.portfolioblock.ui.state.PortfolioBlockUM
import com.tangem.features.feed.impl.R
@Composable
internal fun PortfolioBlock(state: PortfolioBlockUM, modifier: Modifier = Modifier) {
val isVisible = state is PortfolioBlockUM.AddToken || state is PortfolioBlockUM.Content
val screenHeightPx = with(LocalDensity.current) { LocalWindowSize.current.height.toPx() }
Box(modifier = modifier) {
AnimatedVisibility(
visible = isVisible,
enter = fadeIn(animationSpec = tween(durationMillis = 300)),
) {
BottomFade(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
)
}
AnimatedVisibility(
visible = isVisible,
enter = fadeIn(animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)),
exit = fadeOut(animationSpec = tween(durationMillis = 300)),
) {
val slideOffset by transition.animateFloat(
transitionSpec = {
tween(
durationMillis = 600,
easing = CubicBezierEasing(a = 0.83f, b = 0f, c = 0.17f, d = 1f),
)
},
label = "slideOffset",
) { enterExitState ->
when (enterExitState) {
EnterExitState.PreEnter -> screenHeightPx
EnterExitState.Visible -> 0f
EnterExitState.PostExit -> 0f
}
}
Box(modifier = Modifier.graphicsLayer { translationY = slideOffset }) {
when (state) {
is PortfolioBlockUM.AddToken -> AddTokenBlock(state)
is PortfolioBlockUM.Content -> ContentBlock(state)
is PortfolioBlockUM.Hidden,
is PortfolioBlockUM.Loading,
-> Unit
}
}
}
}
}
@Composable
private fun ContentBlock(state: PortfolioBlockUM.Content, modifier: Modifier = Modifier) {
FloatingCard(
modifier = modifier,
onClick = state.onClick,
) {
TangemRowContainer(
contentPadding = PaddingValues(0.dp),
) {
CurrencyIcon(
modifier = Modifier
.padding(end = TangemTheme.dimens2.x3)
.layoutId(TangemRowLayoutId.HEAD),
state = state.tokenIcon,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
text = state.tokenName,
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
text = stringResourceSafe(R.string.markets_portfolio_block_subtitle),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.END_TOP),
text = state.totalBalance.orMaskWithStars(state.isBalanceHidden).resolveAnnotatedReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = TangemTheme.colors2.text.neutral.primary,
style = TangemTheme.typography2.bodyMedium16,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.END_BOTTOM),
text = state.tokenSymbol,
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
)
TangemButton(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x3)
.layoutId(TangemRowLayoutId.TAIL),
buttonUM = TangemButtonUM(
type = TangemButtonType.Secondary,
iconRes = R.drawable.ic_chevron_24,
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X9,
onClick = state.onClick,
),
)
}
}
}
@Composable
private fun AddTokenBlock(state: PortfolioBlockUM.AddToken, modifier: Modifier = Modifier) {
FloatingCard(
modifier = modifier,
onClick = state.onClick,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
CurrencyIcon(state.tokenIcon)
SpacerW(TangemTheme.dimens2.x3)
Text(
text = formatAnnotatedWithBoldColor(
rawString = stringResourceSafe(R.string.markets_portfolio_block_add_token_title),
boldColor = TangemTheme.colors2.text.neutral.primary,
),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
modifier = Modifier.weight(1f),
maxLines = 2,
)
SpacerW(TangemTheme.dimens2.x2)
TangemButton(
buttonUM = TangemButtonUM(
type = TangemButtonType.Secondary,
text = resourceReference(R.string.common_add),
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X9,
onClick = state.onClick,
),
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun FloatingCard(modifier: Modifier = Modifier, onClick: () -> Unit = {}, content: @Composable () -> Unit) {
Box(
modifier = modifier
.navigationBarsPadding()
.fillMaxWidth()
.widthIn(max = BottomSheetDefaults.SheetMaxWidth)
.padding(
start = TangemTheme.dimens2.x2,
end = TangemTheme.dimens2.x2,
bottom = TangemTheme.dimens2.x2,
)
.clip(RoundedCornerShape(size = TangemTheme.dimens2.x5))
.background(
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(size = TangemTheme.dimens2.x5),
)
.clickable(onClick = onClick)
.padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x3),
) {
content()
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreview() {
TangemThemePreviewRedesign {
PortfolioBlock(
state = PortfolioBlockUM.Content(
totalBalance = stringReference("$1,234.56"),
tokensInPortfolioCount = 3,
tokenIcon = previewCoinIcon,
tokenName = "Bitcoin",
tokenSymbol = "BTC",
isBalanceHidden = false,
onClick = {},
),
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AddTokenPreview() {
TangemThemePreviewRedesign {
PortfolioBlock(
state = PortfolioBlockUM.AddToken(
tokenIcon = previewCoinIcon,
onClick = {},
),
)
}
}
private val previewCoinIcon
get() = CurrencyIconState.CoinIcon(
url = null,
fallbackResId = com.tangem.core.ui.R.drawable.img_polygon_22,
isGrayscale = false,
shouldShowCustomBadge = false,
)

View file

@ -0,0 +1,27 @@
package com.tangem.features.feed.components.market.details.portfolioblock.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal sealed class PortfolioBlockUM {
data object Loading : PortfolioBlockUM()
data object Hidden : PortfolioBlockUM()
data class AddToken(
val tokenIcon: CurrencyIconState,
val onClick: () -> Unit,
) : PortfolioBlockUM()
data class Content(
val totalBalance: TextReference,
val tokensInPortfolioCount: Int,
val tokenIcon: CurrencyIconState,
val tokenName: String,
val tokenSymbol: String,
val isBalanceHidden: Boolean,
val onClick: () -> Unit,
) : PortfolioBlockUM()
}

View file

@ -12,16 +12,22 @@ import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.SpacerH4
@ -64,6 +70,7 @@ internal fun MarketsTokenDetailsContent(
backgroundColor: Color,
modifier: Modifier = Modifier,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
portfolioFloatingBlock: @Composable ((Modifier) -> Unit)?,
) {
Content(
contentPadding = contentPadding,
@ -71,6 +78,7 @@ internal fun MarketsTokenDetailsContent(
backgroundColor = backgroundColor,
state = state,
portfolioBlock = portfolioBlock,
portfolioFloatingBlock = portfolioFloatingBlock,
)
when (state.bottomSheetConfig.content) {
@ -80,7 +88,7 @@ internal fun MarketsTokenDetailsContent(
}
}
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LongMethod")
@Composable
private fun Content(
contentPadding: PaddingValues,
@ -88,6 +96,7 @@ private fun Content(
backgroundColor: Color,
modifier: Modifier = Modifier,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
portfolioFloatingBlock: @Composable ((Modifier) -> Unit)?,
) {
val isRedesignEnabled = LocalRedesignEnabled.current
val density = LocalDensity.current
@ -97,54 +106,71 @@ private fun Content(
lazyListState = lazyListState,
onShouldShowPriceSubtitleChange = state.onShouldShowPriceSubtitleChange,
)
var bottomSpacing by remember { mutableStateOf(0.dp) }
Column(
modifier = modifier
Box(
modifier
.drawBehind { drawRect(backgroundColor) }
.fillMaxSize(),
) {
SpacerH4()
Column {
SpacerH4()
LazyColumn(
state = lazyListState,
contentPadding = PaddingValues(bottom = bottomBarHeight, top = contentPadding.calculateTopPadding()),
) {
item("header") {
Header(
state = state,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
}
item { SpacerH16() }
item("intervalSelector") {
IntervalSelector(
trendInterval = state.selectedInterval,
onIntervalClick = state.onSelectedIntervalChange,
isEnabled = state.body !is MarketsTokenDetailsUM.Body.Nothing,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
}
item { SpacerH32() }
item("chart") {
MarketTokenDetailsChart(
modifier = Modifier.fillMaxWidth(),
backgroundColor = backgroundColor,
state = state.chartState,
)
}
item { SpacerH16() }
LazyColumn(
state = lazyListState,
contentPadding = PaddingValues(bottom = bottomBarHeight, top = contentPadding.calculateTopPadding()),
) {
item("header") {
Header(
state = state,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
}
item { SpacerH16() }
item("intervalSelector") {
IntervalSelector(
trendInterval = state.selectedInterval,
onIntervalClick = state.onSelectedIntervalChange,
isEnabled = state.body !is MarketsTokenDetailsUM.Body.Nothing,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
}
item { SpacerH32() }
item("chart") {
MarketTokenDetailsChart(
modifier = Modifier.fillMaxWidth(),
backgroundColor = backgroundColor,
state = state.chartState,
)
}
item { SpacerH16() }
tokenMarketDetailsBody(
state = state.body,
portfolioBlock = portfolioBlock,
relatedNews = state.relatedNews,
isRedesignEnabled = isRedesignEnabled,
)
tokenMarketDetailsBody(
state = state.body,
portfolioBlock = portfolioBlock,
relatedNews = state.relatedNews,
isRedesignEnabled = isRedesignEnabled,
)
item { SpacerH(bottomSpacing) }
}
}
portfolioFloatingBlock?.invoke(
Modifier
.onSizeChanged { size ->
bottomSpacing = if (size.height > 0) {
with(density) { size.height.toDp() + 16.dp }
} else {
0.dp
}
}
.align(Alignment.BottomCenter),
)
}
}
@ -404,6 +430,7 @@ private fun MarketsTokenDetailsContent_Preview(
state = params,
backgroundColor = TangemTheme.colors.background.tertiary,
portfolioBlock = {},
portfolioFloatingBlock = null,
contentPadding = PaddingValues(),
)
}