Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-14 15:30:46 +03:00
parent ddab2eb560
commit 3aa098dcd4
4 changed files with 141 additions and 164 deletions

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap.converters
import com.tangem.common.Provider
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
@ -8,7 +9,8 @@ import com.tangem.feature.swap.domain.models.ui.TokenWithBalance
import com.tangem.feature.swap.models.Network
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
import com.tangem.feature.swap.models.TokenBalanceData
import com.tangem.feature.swap.models.TokenToSelect
import com.tangem.feature.swap.models.TokenToSelectState
import kotlinx.collections.immutable.toImmutableList
class TokensDataConverter(
private val onSearchEntered: (String) -> Unit,
@ -18,21 +20,27 @@ class TokensDataConverter(
fun convertWithNetwork(value: FoundTokensState, network: NetworkInfo): SwapSelectTokenStateHolder {
return SwapSelectTokenStateHolder(
addedTokens = value.tokensInWallet.map { tokenWithBalanceToTokenToSelect(it) },
otherTokens = value.loadedTokens.map { tokenWithBalanceToTokenToSelect(it) },
availableTokens = value.tokensInWallet.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(),
unavailableTokens = value.loadedTokens.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(),
onSearchEntered = onSearchEntered,
onTokenSelected = onTokenSelected,
network = Network(network.name, network.blockchainId),
)
}
private fun tokenWithBalanceToTokenToSelect(tokenWithBalance: TokenWithBalance): TokenToSelect {
return TokenToSelect(
private fun tokenWithBalanceToTokenToSelect(tokenWithBalance: TokenWithBalance): TokenToSelectState.TokenToSelect {
return TokenToSelectState.TokenToSelect(
id = tokenWithBalance.token.id,
name = tokenWithBalance.token.name,
symbol = tokenWithBalance.token.symbol,
iconUrl = tokenWithBalance.token.logoUrl,
isNative = tokenWithBalance.token is Currency.NativeToken,
// todo replace converting
tokenIcon = TokenIconState.CoinIcon(
url = "",
fallbackResId = 0,
isGrayscale = false,
showCustomBadge = false,
),
addedTokenBalanceData = TokenBalanceData(
amount = tokenWithBalance.tokenBalanceData?.amount,
amountEquivalent = tokenWithBalance.tokenBalanceData?.amountEquivalent,

View file

@ -1,28 +1,36 @@
package com.tangem.feature.swap.models
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import kotlinx.collections.immutable.ImmutableList
data class SwapSelectTokenStateHolder(
val addedTokens: List<TokenToSelect>,
val otherTokens: List<TokenToSelect>,
val availableTokens: ImmutableList<TokenToSelectState>,
val unavailableTokens: ImmutableList<TokenToSelectState>,
val network: Network,
val onSearchEntered: (String) -> Unit,
val onTokenSelected: (String) -> Unit,
)
data class TokenToSelect(
val id: String,
val name: String,
val symbol: String,
val iconUrl: String,
val isNative: Boolean,
val available: Boolean = true,
val addedTokenBalanceData: TokenBalanceData? = null,
)
data class Network(
val name: String,
val blockchainId: String,
)
sealed class TokenToSelectState {
data class Title(val title: String) : TokenToSelectState()
data class TokenToSelect(
val id: String,
val name: String,
val symbol: String,
val isNative: Boolean,
val tokenIcon: TokenIconState,
val available: Boolean = true,
val addedTokenBalanceData: TokenBalanceData? = null,
) : TokenToSelectState()
}
data class TokenBalanceData(
val amount: String?,
val amountEquivalent: String?,

View file

@ -274,11 +274,18 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider:
isBalanceHidden = isBalanceHidden,
)
val selectTokenState = uiState.selectTokenState?.copy(
addedTokens = uiState.selectTokenState.addedTokens.map {
it.copy(
addedTokenBalanceData = it.addedTokenBalanceData?.copy(isBalanceHidden = isBalanceHidden),
)
},
availableTokens = uiState.selectTokenState.availableTokens.map {
when (it) {
is TokenToSelectState.TokenToSelect -> {
it.copy(
addedTokenBalanceData = it.addedTokenBalanceData?.copy(isBalanceHidden = isBalanceHidden),
)
}
is TokenToSelectState.Title -> {
it
}
}
}.toImmutableList(),
)
return uiState.copy(

View file

@ -1,45 +1,31 @@
package com.tangem.feature.swap.ui
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.Divider
import androidx.compose.material.Scaffold
import androidx.compose.material.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.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.common.Strings
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.CurrencyPlaceholderIcon
import com.tangem.core.ui.components.SpacerW2
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.appbar.ExpandableSearchView
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
import com.tangem.feature.swap.models.Network
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
import com.tangem.feature.swap.models.TokenBalanceData
import com.tangem.feature.swap.models.TokenToSelect
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.presentation.R
import kotlinx.coroutines.launch
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) {
@ -64,7 +50,6 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit)
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = Modifier) {
val screenBackgroundColor = TangemTheme.colors.background.secondary
@ -74,44 +59,45 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier =
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (state.addedTokens.isNotEmpty()) {
stickyHeader { Header(title = R.string.swapping_token_list_your_tokens) }
}
item { SpacerH8() }
itemsIndexed(items = state.addedTokens) { index, item ->
TokenItem(
token = item,
network = state.network,
screenBackgroundColor = screenBackgroundColor,
onTokenClick = {
state
.onTokenSelected(item.id)
},
)
tokensToSelectItems(state.availableTokens, state.onTokenSelected)
if (index != state.addedTokens.lastIndex) {
Divider(
color = TangemTheme.colors.stroke.primary,
startIndent = TangemTheme.dimens.spacing54,
item { SpacerH12() }
tokensToSelectItems(state.unavailableTokens, state.onTokenSelected)
item { SpacerH12() }
}
}
private fun LazyListScope.tokensToSelectItems(
items: ImmutableList<TokenToSelectState>,
onTokenClick: (String) -> Unit,
) {
itemsIndexed(items = items) { index, item ->
when (item) {
is TokenToSelectState.Title -> {
TitleHeader(
item = item,
modifier = Modifier.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = items.lastIndex,
),
)
}
}
if (state.otherTokens.isNotEmpty()) {
stickyHeader { Header(title = R.string.swapping_token_list_other_tokens) }
}
itemsIndexed(items = state.otherTokens) { index, item ->
TokenItem(
token = item,
network = state.network,
screenBackgroundColor = screenBackgroundColor,
onTokenClick = { state.onTokenSelected(item.id) },
)
if (index != state.otherTokens.lastIndex) {
Divider(
color = TangemTheme.colors.stroke.primary,
startIndent = TangemTheme.dimens.spacing54,
is TokenToSelectState.TokenToSelect -> {
TokenItem(
token = item,
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = items.lastIndex,
)
.background(TangemTheme.colors.background.action),
onTokenClick = {
onTokenClick(item.id)
},
)
}
}
@ -119,27 +105,35 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier =
}
@Composable
private fun Header(@StringRes title: Int) {
Text(
text = stringResource(id = title).uppercase(),
style = TangemTheme.typography.overline,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
.padding(
vertical = TangemTheme.dimens.spacing6,
horizontal = TangemTheme.dimens.spacing16,
),
textAlign = TextAlign.Start,
)
.background(TangemTheme.colors.background.action),
) {
Text(
text = item.title,
style = TangemTheme.typography.overline,
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing16,
start = TangemTheme.dimens.spacing16,
),
)
}
}
@Suppress("LongMethod")
@Composable
private fun TokenItem(token: TokenToSelect, network: Network, screenBackgroundColor: Color, onTokenClick: () -> Unit) {
private fun TokenItem(
token: TokenToSelectState.TokenToSelect,
onTokenClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = Modifier
modifier = modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size72)
.clickable(onClick = onTokenClick)
.padding(
vertical = TangemTheme.dimens.spacing14,
@ -148,12 +142,15 @@ private fun TokenItem(token: TokenToSelect, network: Network, screenBackgroundCo
verticalAlignment = Alignment.CenterVertically,
) {
TokenIcon(
token = token,
screenBackgroundColor = screenBackgroundColor,
iconPlaceholder = if (token.isNative) getActiveIconRes(network.blockchainId) else null,
state = token.tokenIcon,
shouldDisplayNetwork = true,
)
Column(modifier = Modifier.align(Alignment.CenterVertically)) {
Column(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = TangemTheme.dimens.spacing12),
) {
Text(
text = token.name,
style = TangemTheme.typography.subtitle1,
@ -213,65 +210,16 @@ private fun TokenItem(token: TokenToSelect, network: Network, screenBackgroundCo
}
}
@Suppress("MagicNumber")
@Composable
private fun TokenIcon(token: TokenToSelect, screenBackgroundColor: Color, @DrawableRes iconPlaceholder: Int?) {
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
val isDarkTheme = isSystemInDarkTheme()
val coroutineScope = rememberCoroutineScope()
val data = token.iconUrl.ifEmpty {
iconPlaceholder
}
Box(
modifier = Modifier
.padding(end = TangemTheme.dimens.spacing12)
.background(
color = iconBackgroundColor,
shape = TangemTheme.shapes.roundedCorners8,
),
) {
val iconModifier = Modifier
.size(TangemTheme.dimens.size40)
val colorFilter = if (!token.available) {
val matrix = ColorMatrix().apply { setToSaturation(0f) }
ColorFilter.colorMatrix(matrix)
} else {
null
}
SubcomposeAsyncImage(
modifier = iconModifier,
model = ImageRequest.Builder(LocalContext.current)
.data(data)
.crossfade(true)
.allowHardware(false)
.listener(
onSuccess = { _, result ->
if (isDarkTheme) {
coroutineScope.launch {
val color = ImageBackgroundContrastChecker(
drawable = result.drawable,
backgroundColor = screenBackgroundColor.toArgb(),
).getContrastColorIfNeeded(isDarkTheme)
iconBackgroundColor = color
}
}
},
).build(),
contentDescription = token.id,
loading = { CircleShimmer(modifier = iconModifier) },
error = { CurrencyPlaceholderIcon(modifier = iconModifier, id = token.id) },
alpha = if (!token.available) 0.7f else 1f,
colorFilter = colorFilter,
)
}
}
private val token = TokenToSelect(
private val token = TokenToSelectState.TokenToSelect(
tokenIcon = TokenIconState.CoinIcon(
url = "",
fallbackResId = 0,
isGrayscale = false,
showCustomBadge = false,
),
id = "",
name = "USDC",
symbol = "USDC",
iconUrl = "",
isNative = false,
addedTokenBalanceData = TokenBalanceData(
amount = "15 000 $",
@ -281,17 +229,23 @@ private val token = TokenToSelect(
),
)
private val title = TokenToSelectState.Title(
title = "MY TOKENS",
)
@Preview
@Composable
private fun TokenScreenPreview() {
SwapSelectTokenScreen(
state = SwapSelectTokenStateHolder(
addedTokens = listOf(token, token, token),
otherTokens = listOf(token, token, token),
onSearchEntered = {},
onTokenSelected = {},
network = Network("Ethereum", "ETH"),
),
onBack = {},
)
TangemTheme(isDark = false) {
SwapSelectTokenScreen(
state = SwapSelectTokenStateHolder(
availableTokens = listOf(title, token, token, token).toImmutableList(),
unavailableTokens = listOf(title, token, token, token).toImmutableList(),
onSearchEntered = {},
onTokenSelected = {},
network = Network("Ethereum", "ETH"),
),
onBack = {},
)
}
}