Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-19 13:59:09 +05:00
parent 16fd81429c
commit a7f095c56d
10 changed files with 292 additions and 45 deletions

View file

@ -127,6 +127,7 @@ fun TangemPagerIndicator(
Box(
modifier = modifier
.width(getSize(totalPages))
.conditionalCompose(colors.overlay != null) {
colors.overlay?.let { overlay ->
background(
@ -170,6 +171,19 @@ fun TangemPagerIndicator(
}
}
@Suppress("MagicNumber")
@Composable
private fun getSize(pageCount: Int): Dp {
return when (pageCount) {
0 -> TangemTheme.dimens2.x0
1 -> TangemTheme.dimens2.x8
2 -> TangemTheme.dimens2.x12
3 -> TangemTheme.dimens2.x16
4 -> TangemTheme.dimens2.x20
else -> TangemTheme.dimens2.x24
}
}
private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair<Int, Int> {
if (totalPages <= MAX_VISIBLE_DOTS) {
return 0 to totalPages

View file

@ -70,6 +70,8 @@ internal interface WalletContentClickIntents {
fun onManageTokensClick(accountId: AccountId)
fun onManageTokensClick(userWalletId: UserWalletId)
fun onTransactionClick(txHash: String)
fun onDissmissBottomSheet()
@ -228,6 +230,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
router.openManageTokensScreen(accountId)
}
override fun onManageTokensClick(userWalletId: UserWalletId) {
router.openManageTokensScreen(
AccountId.forMainCryptoPortfolio(userWalletId),
)
}
private fun openYieldSupply(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, apy: String) {
router.openYieldSupplyEntryScreen(
userWalletId = userWalletId,

View file

@ -42,6 +42,27 @@ internal object WalletScreenPreviewData {
onItemLongClick = {},
)
private val accountRowDefault = TangemTokenRowUM.Content(
id = "1",
headIconUM = TangemIconUM.Currency(CurrencyIconState.Loading),
titleUM = TangemTokenRowUM.TitleUM.Content(
text = stringReference("Main Account"),
),
subtitleUM = TangemTokenRowUM.SubtitleUM.Content(
text = stringReference("2 tokens"),
),
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference("1 234,56 \$"),
),
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference("0,12345678 BTC"),
),
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
tailUM = TangemRowTailUM.Empty,
onItemClick = {},
onItemLongClick = {},
)
private val tokenListDefault = WalletTokensListUM.Content(
tokenList = persistentListOf(
TokensListItemUM2.Token(tokenRowDefault.copy(id = "0")),
@ -58,6 +79,66 @@ internal object WalletScreenPreviewData {
),
)
private val tokenListAccounts = WalletTokensListUM.Content(
tokenList = persistentListOf(
TokensListItemUM2.Portfolio(
tokenRowUM = accountRowDefault.copy(id = "0"),
tokenList = persistentListOf(
TokensListItemUM2.Token(tokenRowDefault.copy(id = "1")),
TokensListItemUM2.Token(tokenRowDefault.copy(id = "2")),
),
isExpanded = true,
isCollapsable = true,
onEmptyClick = {},
),
TokensListItemUM2.Portfolio(
tokenRowUM = accountRowDefault.copy(id = "3"),
tokenList = persistentListOf(
TokensListItemUM2.Token(tokenRowDefault.copy(id = "4")),
TokensListItemUM2.Token(tokenRowDefault.copy(id = "5")),
),
isExpanded = true,
isCollapsable = true,
onEmptyClick = {},
),
),
organizeButtonUM = TangemButtonUM(
text = resourceReference(R.string.organize_tokens_title),
type = TangemButtonType.Secondary,
onClick = {},
iconRes = R.drawable.ic_filter_default_24,
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
),
)
private val tokenListAccountsWithEmpty = WalletTokensListUM.Content(
tokenList = persistentListOf(
TokensListItemUM2.Portfolio(
tokenRowUM = accountRowDefault.copy(id = "0"),
tokenList = persistentListOf(),
isExpanded = true,
isCollapsable = true,
onEmptyClick = {},
),
TokensListItemUM2.Portfolio(
tokenRowUM = accountRowDefault.copy(id = "3"),
tokenList = persistentListOf(),
isExpanded = false,
isCollapsable = true,
onEmptyClick = {},
),
),
organizeButtonUM = TangemButtonUM(
text = resourceReference(R.string.organize_tokens_title),
type = TangemButtonType.Secondary,
onClick = {},
iconRes = R.drawable.ic_filter_default_24,
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
),
)
private val walletLocked = WalletUM.Locked(
walletsBalanceUM = WalletBalancePreview.empty,
buttons = WalletPreviewData.disabledActionButtons,
@ -99,11 +180,20 @@ internal object WalletScreenPreviewData {
),
notifications = persistentListOf(),
notificationsCarousel = persistentListOf(),
tokensListUM = WalletTokensListUM.Empty,
tokensListUM = WalletTokensListUM.Empty(onEmptyClick = {}),
nftState = WalletNFTItemUM.Hidden,
tangemPayState = TangemPayState.Empty,
)
private val walletAccountDefault = walletDefault.copy(
tokensListUM = tokenListAccounts,
)
private val walletAccountEmpty = walletEmpty.copy(
walletsBalanceUM = WalletBalancePreview.content,
tokensListUM = tokenListAccountsWithEmpty,
)
val defaultState = WalletScreenState(
topBarConfig = topBarConfig,
selectedWalletIndex = 0,
@ -112,6 +202,8 @@ internal object WalletScreenPreviewData {
walletDefault,
walletEmpty,
walletLocked,
walletAccountDefault,
walletAccountEmpty,
),
onWalletChange = { _, _ -> },
event = consumedEvent(),
@ -120,10 +212,18 @@ internal object WalletScreenPreviewData {
onDismissMarketsTooltip = {},
)
val defaultAccountState = defaultState.copy(
selectedWalletIndex = 3,
)
val emptyState = defaultState.copy(
selectedWalletIndex = 1,
)
val emptyAccountState = defaultState.copy(
selectedWalletIndex = 4,
)
val lockedState = defaultState.copy(
selectedWalletIndex = 2,
)

View file

@ -20,7 +20,9 @@ internal sealed class WalletTokensListUM {
abstract val tokenList: ImmutableList<TokensListItemUM2>
abstract val organizeButtonUM: TangemButtonUM?
data object Empty : WalletTokensListUM() {
data class Empty(
val onEmptyClick: () -> Unit,
) : WalletTokensListUM() {
override val tokenList: ImmutableList<TokensListItemUM2.Portfolio> = persistentListOf()
override val organizeButtonUM: TangemButtonUM? = null
}
@ -32,12 +34,14 @@ internal sealed class WalletTokensListUM {
tokenList = persistentListOf(),
isExpanded = false,
isCollapsable = true,
onEmptyClick = {},
),
TokensListItemUM2.Portfolio(
tokenRowUM = TangemTokenRowUM.Empty(id = "1"),
tokenList = persistentListOf(),
isExpanded = false,
isCollapsable = true,
onEmptyClick = {},
),
)
override val organizeButtonUM: TangemButtonUM? = null
@ -50,18 +54,21 @@ internal sealed class WalletTokensListUM {
tokenList = persistentListOf(),
isExpanded = false,
isCollapsable = true,
onEmptyClick = {},
),
TokensListItemUM2.Portfolio(
tokenRowUM = TangemTokenRowUM.Loading(id = "1"),
tokenList = persistentListOf(),
isExpanded = false,
isCollapsable = true,
onEmptyClick = {},
),
TokensListItemUM2.Portfolio(
tokenRowUM = TangemTokenRowUM.Loading(id = "2"),
tokenList = persistentListOf(),
isExpanded = false,
isCollapsable = true,
onEmptyClick = {},
),
)
override val organizeButtonUM: TangemButtonUM? = null
@ -90,6 +97,7 @@ internal sealed interface TokensListItemUM2 {
data class Portfolio(
override val tokenRowUM: TangemTokenRowUM,
val onEmptyClick: () -> Unit,
val tokenList: ImmutableList<TokensListItemUM2>,
val isExpanded: Boolean,
val isCollapsable: Boolean,

View file

@ -9,6 +9,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.common.util.getCardsCount
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.utils.disableButtons
@ -19,6 +20,7 @@ internal class SetTokenListErrorTransformer(
private val selectedWallet: UserWallet,
private val error: TokenListError,
private val appCurrency: AppCurrency,
private val clickIntents: WalletClickIntents,
) : WalletStateTransformer(selectedWallet.walletId) {
override fun transform(prevState: WalletState): WalletState {
@ -57,7 +59,11 @@ internal class SetTokenListErrorTransformer(
is WalletUM.Content -> {
walletUM.copy(
walletsBalanceUM = walletUM.walletsBalanceUM.toLoadedState(),
tokensListUM = WalletTokensListUM.Empty,
tokensListUM = WalletTokensListUM.Empty(
onEmptyClick = {
clickIntents.onManageTokensClick(walletUM.walletsBalanceUM.id)
},
),
buttons = walletUM.disableButtons(),
)
}

View file

@ -7,7 +7,6 @@ import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletBalanceUMTransformer
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMConverter
@ -99,7 +98,13 @@ internal class SetTokenListTransformer(
}
private fun toLoadedState(): WalletTokensListUM {
if (params !is TokenConverterParams.Account) return WalletTokensListUM.Empty
if (params !is TokenConverterParams.Account) {
return WalletTokensListUM.Empty(
onEmptyClick = {
clickIntents.onManageTokensClick(userWallet.walletId)
},
)
}
return WalletTokensListUMConverter(
selectedWallet = userWallet,

View file

@ -72,7 +72,9 @@ internal class WalletTokensListUMConverter(
override fun convert(value: AccountStatusList): WalletTokensListUM {
val promoCryptoCurrency = yieldSupplyPromoBannerConverter.convert2(value = value)
return if (value.flattenCurrencies().isEmpty()) {
WalletTokensListUM.Empty
WalletTokensListUM.Empty(
onEmptyClick = { clickIntents.onManageTokensClick(value.mainAccount.accountId) },
)
} else {
val isCollapsable = value.accountStatuses.count {
it is AccountStatus.CryptoPortfolio && it.account.tokensCount > 0
@ -89,6 +91,7 @@ internal class WalletTokensListUMConverter(
tokenRowUM = accountRowConverter.convert(accountStatus),
isExpanded = isExpanded || !isCollapsable,
isCollapsable = isCollapsable,
onEmptyClick = { clickIntents.onManageTokensClick(accountStatus.account.accountId) },
tokenList = getTokenListItems(
accountStatus,
promoCryptoCurrency,

View file

@ -132,6 +132,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
selectedWallet = userWallet,
error = e,
appCurrency = appCurrency,
clickIntents = clickIntents,
),
)
return

View file

@ -579,6 +579,8 @@ private class WalletScreen2PreviewProvider : PreviewParameterProvider<WalletScre
get() = sequenceOf(
WalletScreenPreviewData.defaultState,
WalletScreenPreviewData.emptyState,
WalletScreenPreviewData.defaultAccountState,
WalletScreenPreviewData.emptyAccountState,
WalletScreenPreviewData.lockedState,
)
}

View file

@ -17,6 +17,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
@ -25,11 +26,15 @@ import androidx.compose.ui.text.lerp
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.tokenlist.TokenListItem
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.ds.button.PrimaryInverseTangemButton
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.header.TangemHeaderRow
@ -120,7 +125,10 @@ internal fun LazyListScope.tokensListItems2(
}
}
}
WalletTokensListUM.Empty -> nonContentItem(modifier = modifier)
is WalletTokensListUM.Empty -> nonContentItem2(
onEmptyClick = walletTokensListUM.onEmptyClick,
modifier = modifier,
)
}
}
@ -168,7 +176,11 @@ private fun LazyListScope.portfolioItem(
isBalanceHidden: Boolean,
modifier: Modifier,
) {
val lastIndex = listItem.tokenList.lastIndex + 1
val lastIndex = if (listItem.tokenList.isEmpty()) {
listItem.tokenList.lastIndex + 2
} else {
listItem.tokenList.lastIndex + 1
}
accountItem(
listItem = listItem,
@ -177,45 +189,54 @@ private fun LazyListScope.portfolioItem(
lastIndex = lastIndex,
isBalanceHidden = isBalanceHidden,
)
itemsIndexed(
items = listItem.tokenList,
key = { _, item -> item.tokenRowUM.id },
contentType = { _, item -> item::class.java },
itemContent = { tokenIndex, item ->
SlideInItemVisibility(
currentIndex = tokenIndex + 1,
lastIndex = lastIndex,
modifier = modifier
.animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null)
.roundedShapeItemDecoration(
radius = 18.dp,
currentIndex = tokenIndex + 1,
addDefaultPadding = false,
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors2.surface.level3,
),
visible = listItem.isExpanded,
) {
val itemModifier = Modifier
.testTag(MainScreenTestTags.TOKEN_LIST_ITEM)
.semantics { lazyListItemPosition = tokenIndex + 1 }
if (listItem.tokenList.isEmpty()) {
nonContentAccountItem(
listItem = listItem,
index = index,
lastIndex = lastIndex,
modifier = modifier,
)
} else {
itemsIndexed(
items = listItem.tokenList,
key = { _, item -> item.tokenRowUM.id },
contentType = { _, item -> item::class.java },
itemContent = { tokenIndex, item ->
SlideInItemVisibility(
currentIndex = tokenIndex + 1,
lastIndex = lastIndex,
modifier = modifier
.animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null)
.roundedShapeItemDecoration(
radius = 18.dp,
currentIndex = tokenIndex + 1,
addDefaultPadding = false,
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors2.surface.level3,
),
visible = listItem.isExpanded,
) {
val itemModifier = Modifier
.testTag(MainScreenTestTags.TOKEN_LIST_ITEM)
.semantics { lazyListItemPosition = tokenIndex + 1 }
when (val tokenRowUM = item.tokenRowUM) {
is TangemTokenRowUM -> TangemTokenRow(
tokenRowUM = tokenRowUM,
isBalanceHidden = isBalanceHidden,
reorderableState = null,
modifier = itemModifier,
)
is TangemHeaderRowUM -> TangemHeaderRow(
headerRowUM = tokenRowUM,
isBalanceHidden = isBalanceHidden,
modifier = itemModifier,
)
when (val tokenRowUM = item.tokenRowUM) {
is TangemTokenRowUM -> TangemTokenRow(
tokenRowUM = tokenRowUM,
isBalanceHidden = isBalanceHidden,
reorderableState = null,
modifier = itemModifier,
)
is TangemHeaderRowUM -> TangemHeaderRow(
headerRowUM = tokenRowUM,
isBalanceHidden = isBalanceHidden,
modifier = itemModifier,
)
}
}
}
},
)
},
)
}
}
@Suppress("MagicNumber")
@ -428,6 +449,54 @@ private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) {
}
}
private fun LazyListScope.nonContentItem2(onEmptyClick: () -> Unit, modifier: Modifier = Modifier) {
item(
key = NON_CONTENT_TOKENS_LIST_KEY,
contentType = NON_CONTENT_TOKENS_LIST_KEY,
) {
NonContentItemContentV2(
onClick = onEmptyClick,
textColor = TangemTheme.colors2.text.neutral.tertiary,
modifier = modifier
.animateItem(fadeInSpec = null, fadeOutSpec = null)
.padding(top = 46.dp),
)
}
}
private fun LazyListScope.nonContentAccountItem(
listItem: TokensListItemUM2.Portfolio,
index: Int,
lastIndex: Int,
modifier: Modifier = Modifier,
) {
item(
key = "$NON_CONTENT_TOKENS_LIST_KEY account-${listItem.tokenRowUM.id}",
contentType = "$NON_CONTENT_TOKENS_LIST_KEY account-${listItem.tokenRowUM.id}",
) {
SlideInItemVisibility(
currentIndex = index + 1,
lastIndex = lastIndex,
modifier = modifier
.animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null)
.roundedShapeItemDecoration(
radius = 18.dp,
addDefaultPadding = false,
currentIndex = 1,
lastIndex = 1,
backgroundColor = TangemTheme.colors2.surface.level3,
),
visible = listItem.isExpanded,
) {
NonContentItemContentV2(
modifier = Modifier.padding(vertical = 36.dp),
textColor = TangemTheme.colors2.text.neutral.secondary,
onClick = listItem.onEmptyClick,
)
}
}
}
@Composable
internal fun NonContentItemContent(modifier: Modifier = Modifier) {
Column(
@ -450,4 +519,35 @@ internal fun NonContentItemContent(modifier: Modifier = Modifier) {
style = TangemTheme.typography.caption2,
)
}
}
@Composable
internal fun NonContentItemContentV2(textColor: Color, modifier: Modifier = Modifier, onClick: () -> Unit) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
painter = painterResource(id = R.drawable.ic_empty_64),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens2.x16),
tint = TangemTheme.colors2.graphic.neutral.quaternary,
)
SpacerH(TangemTheme.dimens2.x5)
Text(
text = stringResourceSafe(id = R.string.main_empty_tokens_list_message),
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing48),
color = textColor,
textAlign = TextAlign.Center,
style = TangemTheme.typography2.bodyRegular14,
)
SpacerH(TangemTheme.dimens2.x2)
PrimaryInverseTangemButton(
text = resourceReference(id = R.string.common_add_tokens),
onClick = onClick,
size = TangemButtonSize.X8,
shape = TangemButtonShape.Rounded,
modifier = Modifier,
)
}
}