Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-07 14:56:51 +05:00
parent 99dfa2a392
commit e511e84508
12 changed files with 238 additions and 92 deletions

View file

@ -0,0 +1,26 @@
package com.tangem.core.ui.decompose
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@Stable
interface ComposableListContentComponent<T> {
val uiState: StateFlow<T>
fun LazyListScope.content(uiState: T, modifier: Modifier)
companion object {
val EMPTY = EmptyComposableListContentComponent
}
}
object EmptyComposableListContentComponent : ComposableListContentComponent<Unit> {
override val uiState: StateFlow<Unit> = MutableStateFlow(Unit)
override fun LazyListScope.content(uiState: Unit, modifier: Modifier) { /* no-op */
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.onramp.selecttoken
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
@ -41,11 +42,13 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state = model.state.collectAsStateWithLifecycle()
val state by model.state.collectAsStateWithLifecycle()
val onrampTokenListState by onrampTokenListComponent.uiState.collectAsStateWithLifecycle()
OnrampSelectToken(
state = state.value,
state = state,
onrampTokenListComponent = onrampTokenListComponent,
onrampTokenListState = onrampTokenListState,
hotCryptoComponent = hotCryptoComponent,
modifier = modifier,
)

View file

@ -21,12 +21,14 @@ import com.tangem.features.onramp.hottokens.HotCryptoComponent
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.selecttoken.entity.OnrampOperationUM
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun OnrampSelectToken(
state: OnrampOperationUM,
onrampTokenListComponent: OnrampTokenListComponent,
onrampTokenListState: TokenListUM,
hotCryptoComponent: HotCryptoComponent?,
modifier: Modifier = Modifier,
) {
@ -50,8 +52,9 @@ internal fun OnrampSelectToken(
)
}
item(key = "token_list", contentType = "token_list") {
onrampTokenListComponent.Content(
with(onrampTokenListComponent) {
content(
uiState = onrampTokenListState,
modifier = Modifier
.padding(top = 8.dp)
.padding(horizontal = 16.dp)

View file

@ -2,6 +2,7 @@ package com.tangem.features.onramp.swap
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.tangem.core.analytics.api.AnalyticsEventHandler
@ -54,12 +55,16 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state = model.state.collectAsStateWithLifecycle()
val state by model.state.collectAsStateWithLifecycle()
val fromTokensState by selectFromTokenListComponent.uiState.collectAsStateWithLifecycle()
val toTokensState by selectToTokenListComponent.uiState.collectAsStateWithLifecycle()
SwapSelectTokens(
state = state.value,
state = state,
selectFromTokenListComponent = selectFromTokenListComponent,
selectFromTokenListState = fromTokensState,
selectToTokenListComponent = selectToTokenListComponent,
selectToTokenListState = toTokensState,
modifier = modifier,
)
}

View file

@ -3,14 +3,15 @@ package com.tangem.features.onramp.swap.availablepairs
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.decompose.ComposableListContentComponent
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import kotlinx.coroutines.flow.StateFlow
/** Token list component that present list of available tokens for swap */
@Stable
internal interface AvailableSwapPairsComponent : ComposableContentComponent {
internal interface AvailableSwapPairsComponent : ComposableListContentComponent<TokenListUM> {
/** Component factory */
interface Factory : ComponentFactory<Params, AvailableSwapPairsComponent>

View file

@ -1,17 +1,17 @@
package com.tangem.features.onramp.swap.availablepairs
import androidx.compose.runtime.Composable
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel
import com.tangem.features.onramp.tokenlist.ui.TokenList
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.ui.onrampTokenList
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.StateFlow
@Stable
internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor(
@ -21,11 +21,11 @@ internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor(
private val model: AvailableSwapPairsModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
override val uiState: StateFlow<TokenListUM>
get() = model.state
TokenList(state = state, modifier = modifier)
override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) {
onrampTokenList(state = uiState)
}
@AssistedFactory

View file

@ -14,6 +14,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -23,6 +24,7 @@ import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponen
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
/**
* Swap select tokens
@ -39,7 +41,9 @@ import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
internal fun SwapSelectTokens(
state: SwapSelectTokensUM,
selectFromTokenListComponent: OnrampTokenListComponent,
selectFromTokenListState: TokenListUM,
selectToTokenListComponent: AvailableSwapPairsComponent,
selectToTokenListState: TokenListUM,
modifier: Modifier = Modifier,
) {
BackHandler(onBack = state.onBackClick)
@ -77,33 +81,33 @@ internal fun SwapSelectTokens(
}
if (state.exchangeFrom is ExchangeCardUM.Empty) {
item(key = "select_from", contentType = "select_from") {
selectFromTokenListComponent.Content(
modifier = Modifier
.padding(horizontal = 16.dp)
.animateItem(),
with(selectFromTokenListComponent) {
content(
uiState = selectFromTokenListState,
modifier = Modifier,
)
}
}
if (state.exchangeFrom is ExchangeCardUM.Filled) {
item(key = "exchange_to", contentType = "exchange_to") {
ExchangeCard(
state = state.exchangeTo,
isBalanceHidden = state.isBalanceHidden,
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 12.dp)
.animateItem(),
)
if (selectToTokenListState.warning != NotificationUM.Warning.SwapNoAvailablePair) {
ExchangeCard(
state = state.exchangeTo,
isBalanceHidden = state.isBalanceHidden,
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 12.dp)
.animateItem(),
)
}
}
if (state.exchangeTo is ExchangeCardUM.Empty) {
item(key = "select_to", contentType = "select_to") {
selectToTokenListComponent.Content(
modifier = Modifier
.padding(horizontal = 16.dp)
.animateItem(),
with(selectToTokenListComponent) {
content(
uiState = selectToTokenListState,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
}

View file

@ -1,17 +1,17 @@
package com.tangem.features.onramp.tokenlist
import androidx.compose.runtime.Composable
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.model.OnrampTokenListModel
import com.tangem.features.onramp.tokenlist.ui.TokenList
import com.tangem.features.onramp.tokenlist.ui.onrampTokenList
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.StateFlow
@Stable
internal class DefaultOnrampTokenListComponent @AssistedInject constructor(
@ -21,11 +21,11 @@ internal class DefaultOnrampTokenListComponent @AssistedInject constructor(
private val model: OnrampTokenListModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
override val uiState: StateFlow<TokenListUM>
get() = model.state
TokenList(state = state, modifier = modifier)
override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) {
onrampTokenList(state = uiState)
}
@AssistedFactory

View file

@ -3,14 +3,15 @@ package com.tangem.features.onramp.tokenlist
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.decompose.ComposableListContentComponent
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.onramp.tokenlist.entity.OnrampOperation
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
/** Token list component that present list of token for multi-currency wallet */
@Stable
internal interface OnrampTokenListComponent : ComposableContentComponent {
internal interface OnrampTokenListComponent : ComposableListContentComponent<TokenListUM> {
/** Component factory */
interface Factory : ComponentFactory<Params, OnrampTokenListComponent>

View file

@ -26,8 +26,7 @@ internal class SetNothingToFoundStateTransformer(
id = emptySearchMessageReference.hashCode(),
text = emptySearchMessageReference,
).let(::add)
}
.toImmutableList(),
}.toImmutableList(),
unavailableItems = persistentListOf(),
isBalanceHidden = isBalanceHidden,
)

View file

@ -6,6 +6,7 @@ import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormatte
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -21,7 +22,13 @@ internal object OnrampTokenItemStateConverterFactory {
): TokenItemStateConverter {
return TokenItemStateConverter(
appCurrency = appCurrency,
subtitleStateProvider = { createSubtitleState(status = it, isAvailable = true) },
subtitleStateProvider = {
createSubtitleState(
status = it,
isAvailable = true,
text = stringReference(value = it.currency.symbol),
)
},
subtitle2StateProvider = ::createSubtitle2State,
fiatAmountStateProvider = {
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = true)
@ -40,7 +47,13 @@ internal object OnrampTokenItemStateConverterFactory {
isAvailable = false,
)
},
subtitleStateProvider = { createSubtitleState(status = it, isAvailable = false) },
subtitleStateProvider = {
createSubtitleState(
status = it,
text = stringReference(value = it.currency.symbol),
isAvailable = false,
)
},
subtitle2StateProvider = ::createSubtitle2State,
fiatAmountStateProvider = {
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = false)
@ -48,12 +61,43 @@ internal object OnrampTokenItemStateConverterFactory {
)
}
private fun createSubtitleState(status: CryptoCurrencyStatus, isAvailable: Boolean): TokenItemState.SubtitleState {
fun createUnavailableItemConverterV2(
appCurrency: AppCurrency,
unavailableErrorText: TextReference,
): TokenItemStateConverter {
return TokenItemStateConverter(
appCurrency = appCurrency,
iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) },
titleStateProvider = {
TokenItemState.TitleState.Content(
text = stringReference(value = it.currency.name),
isAvailable = false,
)
},
subtitleStateProvider = {
createSubtitleState(
status = it,
isAvailable = false,
text = unavailableErrorText,
)
},
subtitle2StateProvider = ::createSubtitle2State,
fiatAmountStateProvider = {
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = false)
},
)
}
private fun createSubtitleState(
status: CryptoCurrencyStatus,
isAvailable: Boolean,
text: TextReference,
): TokenItemState.SubtitleState {
return when (status.value) {
CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading
else -> {
TokenItemState.SubtitleState.TextContent(
value = stringReference(value = status.currency.symbol),
value = text,
isAvailable = isAvailable,
)
}

View file

@ -3,27 +3,28 @@ package com.tangem.features.onramp.tokenlist.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.tokenlist.PortfolioListItem
import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem
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.extensions.conditional
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.BuyTokenScreenTestTags
@ -36,17 +37,21 @@ import kotlinx.collections.immutable.ImmutableList
* Token list
*
* @param state state
* @param modifier modifier
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) {
Column(modifier) {
if (state.warning == null) {
SearchBar(searchBarUM = state.searchBarUM)
} else {
AnimatedContent(targetState = state.warning, label = "") { warning ->
internal fun LazyListScope.onrampTokenList(state: TokenListUM) {
val itemModifier = Modifier.padding(horizontal = 16.dp)
if (state.warning == null) {
searchBarItem(searchBarUM = state.searchBarUM, modifier = itemModifier)
} else {
item("NotificationsKey") {
AnimatedContent(
targetState = state.warning,
label = "",
modifier = itemModifier,
) { warning ->
when (warning) {
is NotificationUM.Warning.OnrampErrorNotification -> {
Notification(
@ -60,31 +65,29 @@ internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) {
}
}
}
}
if (state.availableItems.isNotEmpty()) {
SpacerH12()
ItemsBlock(items = state.availableItems, isBalanceHidden = state.isBalanceHidden)
}
tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden)
if (state.unavailableItems.isNotEmpty()) {
SpacerH12()
ItemsBlock(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden)
}
tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden)
}
private fun LazyListScope.searchBarItem(searchBarUM: SearchBarUM, modifier: Modifier = Modifier) {
item("SearchKey") {
SearchBar(
state = searchBarUM,
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
modifier = modifier,
)
}
}
@Composable
private fun SearchBar(searchBarUM: SearchBarUM) {
SearchBar(
state = searchBarUM,
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
)
}
@Composable
private fun ItemsBlock(items: ImmutableList<TokensListItemUM>, isBalanceHidden: Boolean) {
items.fastForEachIndexed { index, item ->
key(item.id) {
private fun LazyListScope.tokensList(items: ImmutableList<TokensListItemUM>, isBalanceHidden: Boolean) {
itemsIndexed(
items = items,
key = { _, item -> item.id },
contentType = { _, item -> item::class.java },
itemContent = { index, item ->
TokenListItem(
state = item,
isBalanceHidden = isBalanceHidden,
@ -92,13 +95,70 @@ private fun ItemsBlock(items: ImmutableList<TokensListItemUM>, isBalanceHidden:
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = items.lastIndex,
addDefaultPadding = false,
backgroundColor = TangemTheme.colors.background.primary,
)
.testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
.semantics { lazyListItemPosition = index },
)
}
},
)
}
internal fun LazyListScope.portfolioTokensList(portfolio: TokensListItemUM.Portfolio, isBalanceHidden: Boolean) {
val tokens = portfolio.tokens
val isExpanded = portfolio.isExpanded
portfolioItem(
portfolio = portfolio,
modifier = Modifier.padding(top = 8.dp),
isBalanceHidden = isBalanceHidden,
)
if (!isExpanded) return
itemsIndexed(
items = tokens,
key = { _, item -> item.id },
contentType = { _, item -> item::class.java },
itemContent = { tokenIndex, token ->
val indexWithHeader = tokenIndex.inc()
PortfolioTokensListItem(
state = token,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.animateItem()
.roundedShapeItemDecoration(
currentIndex = indexWithHeader,
lastIndex = tokens.lastIndex.inc(),
backgroundColor = TangemTheme.colors.background.primary,
)
.conditional(tokenIndex == tokens.lastIndex) {
Modifier.padding(bottom = 8.dp)
},
)
},
)
}
private fun LazyListScope.portfolioItem(
portfolio: TokensListItemUM.Portfolio,
modifier: Modifier,
isBalanceHidden: Boolean,
) {
item(
key = "account-${portfolio.id}",
contentType = "account",
) {
PortfolioListItem(
state = portfolio,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.animateItem()
.roundedShapeItemDecoration(
currentIndex = 0,
lastIndex = portfolio.tokens.lastIndex.inc(),
backgroundColor = TangemTheme.colors.background.primary,
)
.then(modifier),
)
}
}
@ -107,12 +167,12 @@ private fun ItemsBlock(items: ImmutableList<TokensListItemUM>, isBalanceHidden:
@Composable
private fun Preview_TokenList(@PreviewParameter(PreviewTokenListUMProvider::class) state: TokenListUM) {
TangemThemePreview {
TokenList(
state = state,
modifier = Modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.secondary)
.padding(16.dp),
)
LazyColumn(
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
) {
onrampTokenList(
state = state,
)
}
}
}