Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-24 09:59:56 +02:00
parent 3da4771d66
commit 3baed1e815
57 changed files with 1797 additions and 370 deletions

View file

@ -0,0 +1,170 @@
package com.tangem.common.ui.markets.tokenselector
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.flicker
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.utils.StringsSigns
@Composable
fun BalanceColumn(balanceState: BalanceDisplayState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
if (isBalanceHidden) {
HiddenBalance(modifier)
return
}
when (balanceState) {
is BalanceDisplayState.Loading -> LoadingBalanceColumn(modifier)
is BalanceDisplayState.Flickering -> FlickeringBalanceColumn(balanceState, modifier)
is BalanceDisplayState.Stale -> StaleBalanceColumn(balanceState, modifier)
is BalanceDisplayState.Unreachable -> UnreachableBalanceColumn(modifier)
is BalanceDisplayState.Loaded -> LoadedBalanceColumn(balanceState, modifier)
}
}
@Composable
private fun BalanceColumnLayout(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(4.dp),
content = content,
)
}
@Composable
private fun LoadingBalanceColumn(modifier: Modifier = Modifier) {
BalanceColumnLayout(modifier) {
RectangleShimmer(modifier = Modifier.size(width = 108.dp, height = 20.dp), radius = 20.dp)
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 16.dp), radius = 20.dp)
}
}
@Composable
private fun FlickeringBalanceColumn(state: BalanceDisplayState.Flickering, modifier: Modifier = Modifier) {
val flickerModifier = Modifier.flicker(isFlickering = true)
BalanceColumnLayout(modifier) {
Text(
modifier = flickerModifier,
text = state.fiatBalance.resolveAnnotatedReference(),
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
modifier = flickerModifier,
text = state.cryptoBalance.resolveReference(),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
@Composable
private fun StaleBalanceColumn(state: BalanceDisplayState.Stale, modifier: Modifier = Modifier) {
BalanceColumnLayout(modifier) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
modifier = Modifier.size(16.dp),
painter = painterResource(id = R.drawable.ic_error_sync_24),
contentDescription = null,
tint = TangemTheme.colors2.markers.iconGray,
)
SpacerW(TangemTheme.dimens2.x0_5)
Text(
text = state.fiatBalance.resolveAnnotatedReference(),
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Text(
text = state.cryptoBalance.resolveReference(),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
@Composable
private fun UnreachableBalanceColumn(modifier: Modifier = Modifier) {
BalanceColumnLayout(modifier) {
Text(
text = StringsSigns.DASH_SIGN,
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResourceSafe(R.string.common_unreachable),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.status.attention,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
SpacerW(TangemTheme.dimens2.x0_5)
Icon(
modifier = Modifier.size(16.dp),
painter = painterResource(id = R.drawable.ic_alert_triange_24),
contentDescription = null,
tint = TangemTheme.colors2.graphic.status.attention,
)
}
}
}
@Composable
private fun LoadedBalanceColumn(state: BalanceDisplayState.Loaded, modifier: Modifier = Modifier) {
BalanceColumnLayout(modifier) {
Text(
text = state.fiatBalance.resolveAnnotatedReference(),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
)
Text(
text = state.cryptoBalance.resolveReference(),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
)
}
}
@Composable
private fun HiddenBalance(modifier: Modifier = Modifier) {
BalanceColumnLayout(modifier) {
Text(
text = StringsSigns.THREE_STARS,
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
)
Text(
text = StringsSigns.THREE_STARS,
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
)
}
}

View file

@ -0,0 +1,82 @@
package com.tangem.common.ui.markets.tokenselector
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.ds.button.*
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.pluralStringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@Composable
fun GroupedUserAssetItem(item: UserAssetItemUM.Grouped, modifier: Modifier = Modifier) {
Box(
modifier = modifier.background(
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
),
) {
TangemRowContainer(
modifier = Modifier.clickable(onClick = item.onClick),
content = {
LayeringIcons(
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x1),
tangemIconUM = item.icon,
count = item.tokensCount,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
text = item.tokenName,
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
text = pluralStringResourceSafe(R.plurals.common_tokens_count, item.tokensCount, item.tokensCount),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
BalanceColumn(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_TOP),
balanceState = item.balanceState,
isBalanceHidden = item.isBalanceHidden,
)
TangemButton(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x2)
.layoutId(TangemRowLayoutId.TAIL),
buttonUM = TangemButtonUM(
type = TangemButtonType.Secondary,
tangemIconUM = TangemIconUM.Icon(
iconRes = R.drawable.ic_chevron_24,
tintReference = { TangemTheme.colors2.graphic.neutral.primary },
),
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X10,
onClick = item.onClick,
),
)
},
)
}
}

View file

@ -0,0 +1,99 @@
package com.tangem.common.ui.markets.tokenselector
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
private const val MIN_STACKED_ICON_COUNT = 1
private const val MAX_STACKED_ICON_COUNT = 3
private const val FIRST_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER = 1
private const val SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER = 2
private const val MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER = 2
private const val FIRST_BACK_LAYER_ICON_ALPHA = 0.4f
private const val SECOND_BACK_LAYER_ICON_ALPHA = 0.2f
@Composable
fun LayeringIcons(
tangemIconUM: TangemIconUM,
modifier: Modifier = Modifier,
count: Int = MIN_STACKED_ICON_COUNT,
layerHorizontalShift: Dp = TangemTheme.dimens2.x1,
iconSize: Dp = TangemTheme.dimens2.x10,
) {
require(count > 0)
val stackTrailingWidth = layerHorizontalShift * SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER
Box(
modifier = modifier.size(
width = iconSize + stackTrailingWidth,
height = iconSize,
),
) {
val baseIconModifier = Modifier
.align(Alignment.TopStart)
.size(iconSize)
if (count >= MAX_STACKED_ICON_COUNT) {
TangemIcon(
tangemIconUM = tangemIconUM,
modifier = baseIconModifier
.offset(x = layerHorizontalShift * SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER)
.alpha(SECOND_BACK_LAYER_ICON_ALPHA),
)
}
if (count >= MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER) {
TangemIcon(
tangemIconUM = tangemIconUM,
modifier = baseIconModifier
.offset(x = layerHorizontalShift * FIRST_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER)
.alpha(FIRST_BACK_LAYER_ICON_ALPHA),
)
}
TangemIcon(
tangemIconUM = tangemIconUM,
modifier = baseIconModifier,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun LayeringIconsPreview() {
val previewCurrencyIcon = TangemIconUM.Currency(
currencyIconState = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = null,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
shouldShowCustomBadge = false,
),
)
TangemThemePreview {
Column(horizontalAlignment = Alignment.End) {
LayeringIcons(count = MIN_STACKED_ICON_COUNT, tangemIconUM = previewCurrencyIcon)
LayeringIcons(
count = MAX_STACKED_ICON_COUNT,
tangemIconUM = previewCurrencyIcon,
)
LayeringIcons(
count = MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER,
tangemIconUM = previewCurrencyIcon,
)
}
}
}

View file

@ -0,0 +1,117 @@
package com.tangem.common.ui.markets.tokenselector
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.res.TangemTheme
@Composable
fun SingleUserAssetItem(shouldUsePriceBlock: Boolean, item: UserAssetItemUM.Single, modifier: Modifier = Modifier) {
TangemRowContainer(
modifier = modifier.clickable(onClick = item.onClick),
content = {
TangemIcon(
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.HEAD)
.size(40.dp)
.padding(end = TangemTheme.dimens2.x1),
tangemIconUM = item.icon,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
text = item.tokenName,
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis,
)
if (shouldUsePriceBlock) {
PriceBlock(
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
priceChangeState = item.priceChangeState,
fiatRate = item.fiatRate,
balanceState = item.balanceState,
)
} else {
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
text = item.networkName,
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis,
)
}
BalanceColumn(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_TOP),
balanceState = item.balanceState,
isBalanceHidden = item.isBalanceHidden,
)
},
)
}
@Composable
private fun PriceBlock(
priceChangeState: PriceChangeState,
fiatRate: String?,
balanceState: BalanceDisplayState,
modifier: Modifier = Modifier,
) {
val isDisabled = remember(balanceState) {
balanceState is BalanceDisplayState.Unreachable
}
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
if (fiatRate != null) {
Text(
text = fiatRate,
style = TangemTheme.typography2.captionMedium12,
color = if (isDisabled) {
TangemTheme.colors2.text.status.disabled
} else {
TangemTheme.colors2.text.neutral.secondary
},
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
AnimatedContent(
targetState = priceChangeState,
contentKey = { it::class },
) { animatedState ->
when (animatedState) {
is PriceChangeState.Content -> {
PriceChangeInPercent(
valueInPercent = animatedState.valueInPercent,
type = animatedState.type,
textStyle = TangemTheme.typography2.captionMedium12,
isDisabled = isDisabled,
)
}
PriceChangeState.Unknown -> Unit
}
}
}
}

View file

@ -0,0 +1,201 @@
package com.tangem.common.ui.markets.tokenselector
import android.content.res.Configuration
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.Fade
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.ds.button.*
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.rememberHazeState
@Composable
fun TokenSelectorBottomSheet(config: TangemBottomSheetConfig, stickyFooter: StickyFooter? = null) {
TangemBottomSheet<TokenSelectorContentUM>(
config = config,
type = TangemBottomSheetType.Modal,
containerColor = TangemTheme.colors2.surface.level2,
content = { content ->
TokenSelectorContent(
content = content,
onDismiss = config.onDismissRequest,
stickyFooter = stickyFooter,
embedded = false,
)
},
)
}
data class StickyFooter(val buttonText: TextReference, val isEnabled: Boolean = true, val onClick: () -> Unit)
@Composable
fun TokenSelectorEmbeddedContent(
content: TokenSelectorContentUM,
stickyFooter: StickyFooter?,
modifier: Modifier = Modifier,
) {
TokenSelectorContent(
content = content,
onDismiss = {},
stickyFooter = stickyFooter,
embedded = true,
modifier = modifier,
)
}
@Composable
private fun TokenSelectorContent(
content: TokenSelectorContentUM,
onDismiss: () -> Unit,
stickyFooter: StickyFooter?,
embedded: Boolean,
modifier: Modifier = Modifier,
) {
val hazeState = rememberHazeState()
var topBarHeight by remember { mutableStateOf(0.dp) }
val topContentPadding = if (embedded) {
TangemTheme.dimens2.x4
} else {
topBarHeight
}
val footerHeight = TangemTheme.dimens2.x14 + TangemTheme.dimens2.x4
val listBottomPadding = TangemTheme.dimens2.x10 + if (stickyFooter != null) footerHeight else 0.dp
Box(modifier = modifier.fillMaxWidth()) {
LazyColumn(
modifier = Modifier.hazeSourceTangem(state = hazeState, 1f),
contentPadding = PaddingValues(
start = TangemTheme.dimens2.x4,
end = TangemTheme.dimens2.x4,
top = topContentPadding,
bottom = listBottomPadding,
),
) {
tokenSelectorSectionItems(content.sections)
}
if (!embedded) {
TokenSelectorSheetTopBar(
modifier = Modifier.align(Alignment.TopEnd),
onDismiss = onDismiss,
hazeState = hazeState,
onChangeHeight = { topBarHeight = it },
)
}
Fade(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.padding(bottom = if (stickyFooter != null) footerHeight else 0.dp),
height = TangemTheme.dimens2.x10,
)
if (stickyFooter != null) {
TangemButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x2)
.fillMaxWidth(),
buttonUM = TangemButtonUM(
type = TangemButtonType.Primary,
text = stickyFooter.buttonText,
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X15,
isEnabled = stickyFooter.isEnabled,
onClick = stickyFooter.onClick,
),
)
}
}
}
@Composable
private fun TokenSelectorSheetTopBar(
hazeState: HazeState,
onDismiss: () -> Unit,
onChangeHeight: (Dp) -> Unit,
modifier: Modifier = Modifier,
) {
val bgColor = TangemTheme.colors2.surface.level2
val density = LocalDensity.current
TangemTopBar(
modifier = modifier
.onGloballyPositioned { coordinates ->
if (coordinates.size.height > 0) {
with(density) {
onChangeHeight(coordinates.size.height.toDp())
}
}
}
.hazeEffectTangem(state = hazeState) {
backgroundColor = bgColor
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
},
type = TangemTopBarType.BottomSheet,
title = resourceReference(R.string.markets_search_portfolio_header),
endContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_close_24),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clickableSingle(onClick = onDismiss)
.padding(TangemTheme.dimens2.x2_5),
)
},
)
}
@Preview
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TokenSelectorBottomSheetPreview(
@PreviewParameter(TokenSelectorContentPreviewProvider::class) content: TokenSelectorContentUM,
) {
TangemThemePreviewRedesign {
TokenSelectorBottomSheet(
config = TangemBottomSheetConfig(
onDismissRequest = {},
content = content,
isShown = true,
),
)
}
}

View file

@ -0,0 +1,117 @@
package com.tangem.common.ui.markets.tokenselector
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.CryptoPortfolioIcon
import kotlinx.collections.immutable.persistentListOf
@Suppress("StringLiteralDuplication")
class TokenSelectorContentPreviewProvider :
CollectionPreviewParameterProvider<TokenSelectorContentUM>(
listOf(
tokenSelectorPreviewSimple(),
tokenSelectorPreviewWithAccountHeaders(),
tokenSelectorPreviewMultiWallet(),
),
)
private fun tokenSelectorPreviewSimple(): TokenSelectorContentUM {
return TokenSelectorContentUM(
sections = persistentListOf(
TokenSelectorSectionUM.TokenGroup(
accountHeader = null,
items = persistentListOf(
previewTokenItem(id = "eth", name = "Ethereum", symbol = "ETH"),
previewTokenItem(id = "btc", name = "Bitcoin", symbol = "BTC"),
),
),
),
)
}
private fun tokenSelectorPreviewWithAccountHeaders(): TokenSelectorContentUM {
val accountIcon = CryptoPortfolioIcon.ofCustomAccount(
value = CryptoPortfolioIcon.Icon.Wallet,
color = CryptoPortfolioIcon.Color.CaribbeanBlue,
)
return TokenSelectorContentUM(
sections = persistentListOf(
TokenSelectorSectionUM.TokenGroup(
accountHeader = AccountHeaderData(
accountName = stringReference(value = "Main account"),
cryptoPortfolioIcon = accountIcon,
),
items = persistentListOf(
previewTokenItem(id = "eth_main", name = "Ethereum", symbol = "ETH"),
),
),
TokenSelectorSectionUM.TokenGroup(
accountHeader = AccountHeaderData(
accountName = stringReference(value = "Trading"),
cryptoPortfolioIcon = accountIcon,
),
items = persistentListOf(
previewTokenItem(id = "sol_trade", name = "Solana", symbol = "SOL"),
previewTokenItem(id = "avax_trade", name = "Avalanche", symbol = "AVAX"),
),
),
),
)
}
private fun tokenSelectorPreviewMultiWallet(): TokenSelectorContentUM {
return TokenSelectorContentUM(
sections = persistentListOf(
TokenSelectorSectionUM.WalletHeader(walletName = "Cold wallet"),
TokenSelectorSectionUM.TokenGroup(
accountHeader = null,
items = persistentListOf(
previewTokenItem(id = "btc_cold", name = "Bitcoin", symbol = "BTC"),
),
),
TokenSelectorSectionUM.WalletHeader(walletName = "Hot wallet"),
TokenSelectorSectionUM.TokenGroup(
accountHeader = null,
items = persistentListOf(
previewTokenItem(id = "eth_hot", name = "Ethereum", symbol = "ETH"),
previewTokenItem(id = "usdt_hot", name = "Tether", symbol = "USDT"),
),
),
),
)
}
private fun previewTokenItem(id: String, name: String, symbol: String): UserAssetItemUM.Single {
val cryptoRef = stringReference(value = "1.234 $symbol")
val fiatRef = stringReference(value = "$1,234.56")
return UserAssetItemUM.Single(
id = id,
icon = TangemIconUM.Currency(
CurrencyIconState.CoinIcon(
url = null,
fallbackResId = R.drawable.ic_ethereumpow_22,
isGrayscale = false,
shouldShowCustomBadge = false,
),
),
tokenName = name,
tokenSymbol = symbol,
fiatRate = "$98,765.43",
priceChangeState = PriceChangeState.Content(
valueInPercent = "+2.34%",
type = PriceChangeType.UP,
),
balanceState = BalanceDisplayState.Loaded(
cryptoBalance = cryptoRef,
fiatBalance = fiatRef,
),
isBalanceHidden = false,
onClick = {},
networkName = "Ethereum",
)
}

View file

@ -0,0 +1,29 @@
package com.tangem.common.ui.markets.tokenselector
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.account.CryptoPortfolioIcon
import kotlinx.collections.immutable.ImmutableList
@Immutable
data class TokenSelectorContentUM(
val sections: ImmutableList<TokenSelectorSectionUM>,
) : TangemBottomSheetConfigContent
@Immutable
data class AccountHeaderData(
val accountName: TextReference,
val cryptoPortfolioIcon: CryptoPortfolioIcon,
)
@Immutable
sealed interface TokenSelectorSectionUM {
data class WalletHeader(val walletName: String) : TokenSelectorSectionUM
data class TokenGroup(
val accountHeader: AccountHeaderData?,
val items: ImmutableList<UserAssetItemUM.Single>,
) : TokenSelectorSectionUM
}

View file

@ -0,0 +1,138 @@
package com.tangem.common.ui.markets.tokenselector
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.common.ui.account.getResId
import com.tangem.common.ui.account.getUiColor
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import kotlinx.collections.immutable.ImmutableList
fun LazyListScope.tokenSelectorSectionItems(sections: ImmutableList<TokenSelectorSectionUM>) {
sections.forEachIndexed { index, section ->
when (section) {
is TokenSelectorSectionUM.WalletHeader -> {
item(key = "wallet_${section.walletName}_$index") {
WalletHeaderSection(section)
}
}
is TokenSelectorSectionUM.TokenGroup -> {
if (section.items.isEmpty()) return@forEachIndexed
if (index > 0 && sections[index - 1] is TokenSelectorSectionUM.TokenGroup) {
item(key = "spacer_before_group_$index") {
SpacerH(TangemTheme.dimens2.x2)
}
}
val lastIndex = if (section.accountHeader != null) {
section.items.size
} else {
section.items.lastIndex.coerceAtLeast(0)
}
section.accountHeader?.let { header ->
item(key = "account_header_$index") {
TokenGroupAccountHeaderRow(
data = header,
modifier = Modifier.tokenGroupRowDecoration(
currentIndex = 0,
lastIndex = lastIndex,
),
)
}
}
val indexOffset = if (section.accountHeader != null) 1 else 0
section.items.forEachIndexed { itemIndex, single ->
item(key = "token_${single.id}_$index") {
SingleUserAssetItem(
item = single,
modifier = Modifier.tokenGroupRowDecoration(
currentIndex = indexOffset + itemIndex,
lastIndex = lastIndex,
),
shouldUsePriceBlock = false,
)
}
}
}
}
}
}
@Composable
private fun Modifier.tokenGroupRowDecoration(currentIndex: Int, lastIndex: Int): Modifier =
this.roundedShapeItemDecoration(
currentIndex = currentIndex,
lastIndex = lastIndex,
addDefaultPadding = false,
radius = TangemTheme.dimens2.x6,
backgroundColor = TangemTheme.colors2.surface.level3,
)
@Composable
private fun WalletHeaderSection(section: TokenSelectorSectionUM.WalletHeader) {
Row(
modifier = Modifier
.padding(top = TangemTheme.dimens2.x4, bottom = TangemTheme.dimens2.x2)
.padding(horizontal = TangemTheme.dimens2.x3),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
Text(
text = section.walletName,
style = TangemTheme.typography2.subheadlineMedium14,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_key_card_20),
modifier = Modifier.size(TangemTheme.dimens2.x5),
tint = TangemTheme.colors2.graphic.neutral.tertiary,
contentDescription = null,
)
}
}
@Composable
private fun TokenGroupAccountHeaderRow(data: AccountHeaderData, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens2.x4, bottom = TangemTheme.dimens2.x2)
.padding(horizontal = TangemTheme.dimens2.x4),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x4),
imageVector = ImageVector.vectorResource(data.cryptoPortfolioIcon.value.getResId()),
tint = data.cryptoPortfolioIcon.color.getUiColor(),
contentDescription = null,
)
Text(
text = data.accountName.resolveReference(),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}

View file

@ -0,0 +1,61 @@
package com.tangem.common.ui.markets.tokenselector
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.TextReference
@Immutable
sealed interface BalanceDisplayState {
data class Loaded(
val cryptoBalance: TextReference,
val fiatBalance: TextReference,
) : BalanceDisplayState
data class Flickering(
val cryptoBalance: TextReference,
val fiatBalance: TextReference,
) : BalanceDisplayState
data class Stale(
val cryptoBalance: TextReference,
val fiatBalance: TextReference,
) : BalanceDisplayState
data object Loading : BalanceDisplayState
data object Unreachable : BalanceDisplayState
}
@Immutable
sealed interface UserAssetItemUM {
val id: String
val icon: TangemIconUM
val tokenName: String
val tokenSymbol: String
val onClick: () -> Unit
data class Single(
override val id: String,
override val icon: TangemIconUM,
override val tokenName: String,
override val tokenSymbol: String,
val fiatRate: String?,
val priceChangeState: PriceChangeState,
val balanceState: BalanceDisplayState,
val isBalanceHidden: Boolean,
val networkName: String,
override val onClick: () -> Unit,
) : UserAssetItemUM
data class Grouped(
override val id: String,
override val icon: TangemIconUM,
override val tokenName: String,
override val tokenSymbol: String,
val tokensCount: Int,
val balanceState: BalanceDisplayState,
val isBalanceHidden: Boolean,
override val onClick: () -> Unit,
) : UserAssetItemUM
}