Updated on 2026-08-14

This commit is contained in:
Tangem 2023-08-18 13:00:01 +03:00
parent cd34a586bb
commit f2abb0ee1c
11 changed files with 159 additions and 90 deletions

View file

@ -5,6 +5,12 @@ plugins {
}
dependencies {
/** Project - Domain */
implementation(projects.domain.tokens.models)
/** Project - Core */
implementation(projects.core.res)
/** AndroidX libraries */
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.paging.runtime)
@ -22,6 +28,4 @@ dependencies {
implementation(deps.material)
implementation(deps.compose.shimmer)
implementation(deps.kotlin.immutable.collections)
implementation(project(":core:res"))
}

View file

@ -0,0 +1,45 @@
package com.tangem.core.ui.extensions
import androidx.annotation.DrawableRes
import com.tangem.core.ui.R
import com.tangem.domain.tokens.models.CryptoCurrency
/**
* Retrieves the resource ID for the network badge of a [CryptoCurrency].
*
* This property provides a way to fetch the appropriate drawable resource ID
* for the network badge of a given cryptocurrency. For coins, this will typically
* return null as they do not have network badges, while tokens will fetch the icon
* based on their associated network ID.
*
* @return Drawable resource ID for the network badge or null if the cryptocurrency is a coin.
*/
@get:DrawableRes
val CryptoCurrency.networkBadgeIconResId: Int?
get() = when (this) {
is CryptoCurrency.Coin -> null
is CryptoCurrency.Token -> getActiveIconRes(network.id.value)
}
/**
* Retrieves the resource ID for the icon of a [CryptoCurrency].
*
* This property provides a way to fetch the appropriate drawable resource ID
* for the icon of a given cryptocurrency.
*
* @return Drawable resource ID for the cryptocurrency icon.
*/
@get:DrawableRes
val CryptoCurrency.iconResId: Int
get() = when (this) {
is CryptoCurrency.Coin -> {
val rawCoinId = id.rawCurrencyId
if (rawCoinId != null) {
getActiveIconResByCoinId(rawCoinId, network.id.value)
} else {
R.drawable.ic_alert_24
}
}
is CryptoCurrency.Token -> R.drawable.ic_alert_24
}

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before After
Before After

View file

@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.iconResId
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
@ -10,7 +11,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSkeletonStateConverter.SkeletonModel
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.converter.Converter
import kotlinx.coroutines.flow.MutableStateFlow
@ -27,13 +27,12 @@ internal class TokenDetailsSkeletonStateConverter(
tokenInfoBlockState = TokenInfoBlockState(
name = value.cryptoCurrency.name,
iconUrl = requireNotNull(value.cryptoCurrency.iconUrl),
currency = when (value.cryptoCurrency) {
currency = when (val currency = value.cryptoCurrency) {
is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native
is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token(
networkName = value.cryptoCurrency.network.standardType.name,
blockchainName = value.cryptoCurrency.network.name,
// TODO: [REDACTED_JIRA]
networkIcon = R.drawable.img_eth_22,
networkName = currency.network.standardType.name,
blockchainName = currency.network.name,
networkIcon = currency.iconResId,
)
},
),

View file

@ -87,7 +87,7 @@ internal object WalletPreviewData {
id = UUID.randomUUID().toString(),
tokenIconUrl = null,
tokenIconResId = R.drawable.img_polygon_22,
networkIconResId = R.drawable.img_polygon_22,
networkBadgeIconResId = R.drawable.img_polygon_22,
name = "Polygon",
amount = "5,412 MATIC",
hasPending = true,
@ -98,16 +98,24 @@ internal object WalletPreviewData {
type = PriceChangeConfig.Type.UP,
),
),
isTestnet = false,
onClick = {},
)
}
val testnetTokenItemVisibleState by lazy {
tokenItemVisibleState.copy(
name = "Polygon testnet",
isTestnet = true,
)
}
val tokenItemHiddenState by lazy {
TokenItemState.Content(
id = UUID.randomUUID().toString(),
tokenIconUrl = null,
tokenIconResId = R.drawable.img_polygon_22,
networkIconResId = R.drawable.img_polygon_22,
networkBadgeIconResId = R.drawable.img_polygon_22,
name = "Polygon",
amount = "5,412 MATIC",
hasPending = true,
@ -117,6 +125,7 @@ internal object WalletPreviewData {
type = PriceChangeConfig.Type.UP,
),
),
isTestnet = false,
onClick = {},
)
}
@ -126,8 +135,9 @@ internal object WalletPreviewData {
id = UUID.randomUUID().toString(),
tokenIconUrl = null,
tokenIconResId = R.drawable.img_polygon_22,
networkIconResId = R.drawable.img_polygon_22,
networkBadgeIconResId = R.drawable.img_polygon_22,
name = "Polygon",
isTestnet = false,
fiatAmount = "3 172,14 $",
)
}
@ -137,7 +147,7 @@ internal object WalletPreviewData {
id = UUID.randomUUID().toString(),
tokenIconUrl = null,
tokenIconResId = R.drawable.img_polygon_22,
networkIconResId = R.drawable.img_polygon_22,
networkBadgeIconResId = R.drawable.img_polygon_22,
name = "Polygon",
)
}
@ -171,7 +181,7 @@ internal object WalletPreviewData {
tokenItemState = tokenItemDragState.copy(
id = "${group.id}_token_$tokenNumber",
name = "Token $tokenNumber from $networkNumber network",
networkIconResId = R.drawable.img_eth_22.takeIf { i != 0 },
networkBadgeIconResId = R.drawable.img_eth_22.takeIf { i != 0 },
),
groupId = group.id,
roundingMode = when {
@ -271,7 +281,7 @@ internal object WalletPreviewData {
id = "token_1",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkIconResId = null,
networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),
@ -280,7 +290,7 @@ internal object WalletPreviewData {
id = "token_2",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkIconResId = null,
networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),
@ -289,7 +299,7 @@ internal object WalletPreviewData {
id = "token_3",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkIconResId = null,
networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),
@ -298,7 +308,7 @@ internal object WalletPreviewData {
id = "token_4",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkIconResId = null,
networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),
@ -308,7 +318,7 @@ internal object WalletPreviewData {
id = "token_5",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkIconResId = null,
networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),

View file

@ -14,9 +14,12 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -42,6 +45,8 @@ import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
private const val DOTS = "•••"
private const val GRAY_SCALE_SATURATION = 0f
val TOKEN_ITEM_HEIGHT: Dp
@Composable
@ReadOnlyComposable
@ -49,6 +54,7 @@ val TOKEN_ITEM_HEIGHT: Dp
@Composable
internal fun TokenItem(state: TokenItemState, modifier: Modifier = Modifier) {
// TODO: Add custom token state: [REDACTED_JIRA]
when (state) {
is TokenItemState.Content -> ContentTokenItem(state, modifier)
is TokenItemState.Loading -> LoadingTokenItem(modifier)
@ -65,7 +71,7 @@ private fun ContentTokenItem(content: TokenItemState.Content, modifier: Modifier
name = content.name,
tokenIconUrl = content.tokenIconUrl,
tokenIconResId = content.tokenIconResId,
networkIconResId = content.networkIconResId,
networkBadgeIconResId = content.networkBadgeIconResId,
amount = if (content.tokenOptions is TokenOptionsState.Hidden) DOTS else content.amount,
hasPending = content.hasPending,
options = { ref ->
@ -74,6 +80,7 @@ private fun ContentTokenItem(content: TokenItemState.Content, modifier: Modifier
state = content.tokenOptions,
)
},
isTestnet = content.isTestnet,
)
}
@ -88,9 +95,10 @@ internal fun DraggableTokenItem(
name = state.name,
tokenIconUrl = state.tokenIconUrl,
tokenIconResId = state.tokenIconResId,
networkIconResId = state.networkIconResId,
networkBadgeIconResId = state.networkBadgeIconResId,
amount = state.fiatAmount,
hasPending = false,
isTestnet = state.isTestnet,
options = { ref ->
Box(
modifier = Modifier
@ -122,7 +130,7 @@ internal fun UnreachableTokenItem(state: TokenItemState.Unreachable, modifier: M
name = state.name,
tokenIconUrl = state.tokenIconUrl,
tokenIconResId = state.tokenIconResId,
networkIconResId = state.networkIconResId,
networkBadgeIconResId = state.networkBadgeIconResId,
amount = null,
hasPending = false,
options = { ref ->
@ -213,11 +221,12 @@ private fun InternalTokenItem(
name: String,
tokenIconUrl: String?,
@DrawableRes tokenIconResId: Int,
@DrawableRes networkIconResId: Int?,
@DrawableRes networkBadgeIconResId: Int?,
amount: String?,
hasPending: Boolean,
options: @Composable ConstraintLayoutScope.(ref: ConstrainedLayoutReference) -> Unit,
modifier: Modifier = Modifier,
isTestnet: Boolean = false,
onClick: (() -> Unit)? = null,
) {
BaseSurface(
@ -241,7 +250,8 @@ private fun InternalTokenItem(
},
tokenIconUrl = tokenIconUrl,
tokenIconResId = tokenIconResId,
networkIconRes = networkIconResId,
networkBadgeIconRes = networkBadgeIconResId,
isTestnet = isTestnet,
)
TokenTitleAmountBlock(
@ -379,9 +389,10 @@ private fun TokenFiatPercentageBlock(
@Composable
private fun TokenIcon(
tokenIconUrl: String?,
@DrawableRes tokenIconResId: Int,
isTestnet: Boolean,
modifier: Modifier = Modifier,
@DrawableRes tokenIconResId: Int? = null,
@DrawableRes networkIconRes: Int? = null,
@DrawableRes networkBadgeIconRes: Int? = null,
) {
Box(
modifier = modifier
@ -392,19 +403,36 @@ private fun TokenIcon(
.align(Alignment.BottomStart)
.size(TangemTheme.dimens.size36)
val data = if (tokenIconUrl.isNullOrEmpty()) tokenIconResId else tokenIconUrl
val iconData: Any = remember(tokenIconUrl) {
if (tokenIconUrl.isNullOrEmpty()) tokenIconResId else tokenIconUrl
}
val colorFilter = remember(isTestnet) {
if (isTestnet) {
val colorMatrix = ColorMatrix().also {
it.setToSaturation(GRAY_SCALE_SATURATION)
}
ColorFilter.colorMatrix(colorMatrix)
} else {
null
}
}
SubcomposeAsyncImage(
modifier = tokenImageModifier,
model = ImageRequest.Builder(LocalContext.current)
.data(data)
.crossfade(true)
.data(iconData)
.placeholder(tokenIconResId)
.error(tokenIconResId)
.fallback(tokenIconResId)
.crossfade(enable = true)
.build(),
loading = { CircleShimmer(modifier = tokenImageModifier) },
colorFilter = colorFilter,
contentDescription = null,
)
AnimatedVisibility(
visible = networkIconRes != null,
visible = networkBadgeIconRes != null,
modifier = Modifier
.align(Alignment.TopEnd)
.size(TangemTheme.dimens.size18)
@ -414,7 +442,8 @@ private fun TokenIcon(
modifier = Modifier
.padding(all = TangemTheme.dimens.spacing0_5)
.align(Alignment.Center),
painter = painterResource(id = requireNotNull(networkIconRes)),
painter = painterResource(id = requireNotNull(networkBadgeIconRes)),
colorFilter = colorFilter,
contentDescription = null,
)
}
@ -446,6 +475,7 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider<TokenItem
WalletPreviewData.tokenItemDragState,
WalletPreviewData.tokenItemHiddenState,
WalletPreviewData.loadingTokenItemState,
WalletPreviewData.testnetTokenItemVisibleState,
),
)

View file

@ -17,60 +17,65 @@ internal sealed interface TokenItemState {
/**
* Content token state
*
* @property id unique id
* @property tokenIconUrl token icon url
* @property tokenIconResId token icon resource id
* @property networkIconResId network icon resource id, may be null if it is a coin
* @property name token name
* @property amount amount of token
* @property hasPending pending tx in blockchain
* @property tokenOptions state for token options
* @property id unique id
* @property tokenIconUrl token icon url
* @property tokenIconResId token icon resource id
* @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin
* @property name token name
* @property amount amount of token
* @property hasPending pending tx in blockchain
* @property tokenOptions state for token options
* @property isTestnet indicates whether the token is from test network or not
* @property onClick callback which will be called when an item is clicked
*/
data class Content(
override val id: String,
val tokenIconUrl: String?,
@DrawableRes val tokenIconResId: Int,
@DrawableRes val networkIconResId: Int?,
@DrawableRes val networkBadgeIconResId: Int?,
val name: String,
val amount: String,
val hasPending: Boolean,
val tokenOptions: TokenOptionsState,
val isTestnet: Boolean,
val onClick: () -> Unit,
) : TokenItemState
/**
* Draggable token state
*
* @property id unique id
* @property tokenIconUrl token icon url
* @property tokenIconResId token icon resource id
* @property networkIconResId network icon resource id, may be null if it is a coin
* @property name token name
* @property fiatAmount fiat amount of token
* @property id unique id
* @property tokenIconUrl token icon url
* @property tokenIconResId token icon resource id
* @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin
* @property name token name
* @property fiatAmount fiat amount of token
* @property isTestnet indicates whether the token is from test network or not
*/
data class Draggable(
override val id: String,
val tokenIconUrl: String?,
@DrawableRes val tokenIconResId: Int,
@DrawableRes val networkIconResId: Int?,
@DrawableRes val networkBadgeIconResId: Int?,
val name: String,
val fiatAmount: String,
val isTestnet: Boolean,
) : TokenItemState
/**
* Unreachable token state
*
* @property id token id
* @property tokenIconUrl token icon url
* @property tokenIconResId token icon resource id
* @property networkIconResId network icon resource id, may be null if it is a coin
* @property name token name
* @property id token id
* @property tokenIconUrl token icon url
* @property tokenIconResId token icon resource id
* @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin
* @property name token name
*/
data class Unreachable(
override val id: String,
val tokenIconUrl: String?,
@DrawableRes val tokenIconResId: Int,
@DrawableRes val networkIconResId: Int?,
@DrawableRes val networkBadgeIconResId: Int?,
val name: String,
) : TokenItemState

View file

@ -1,12 +1,11 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
import androidx.annotation.DrawableRes
import com.tangem.common.Provider
import com.tangem.core.ui.extensions.iconResId
import com.tangem.core.ui.extensions.networkBadgeIconResId
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId
@ -17,18 +16,6 @@ internal class CryptoCurrencyToDraggableItemConverter(
private val appCurrencyProvider: Provider<AppCurrency>,
) : Converter<CryptoCurrencyStatus, DraggableItem.Token> {
private val CryptoCurrency.networkIconResId: Int?
@DrawableRes get() {
// TODO: [REDACTED_JIRA]
return if (this is CryptoCurrency.Coin) null else R.drawable.img_eth_22
}
private val CryptoCurrency.tokenIconResId: Int
@DrawableRes get() {
// TODO: [REDACTED_JIRA]
return R.drawable.img_eth_22
}
override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token {
return createDraggableToken(value, appCurrencyProvider())
}
@ -58,10 +45,11 @@ internal class CryptoCurrencyToDraggableItemConverter(
return TokenItemState.Draggable(
id = getTokenItemId(currency.id),
tokenIconUrl = currency.iconUrl,
tokenIconResId = currency.tokenIconResId,
networkIconResId = currency.networkIconResId,
tokenIconResId = currencyStatus.currency.iconResId,
networkBadgeIconResId = currencyStatus.currency.networkBadgeIconResId,
name = currency.name,
fiatAmount = getFormattedFiatAmount(currencyStatus, appCurrency),
isTestnet = currencyStatus.currency.network.isTestnet,
)
}

View file

@ -54,7 +54,7 @@ internal object WalletAdditionalInfoFactory {
isLocked -> {
backupInfoRes + TextReference.Res(R.string.common_locked)
}
else -> error("It isn't exist additional info for this case")
else -> error("It isn't exist additional info for this case") // FIXME: Crashes on dev cards
}
}

View file

@ -1,13 +1,12 @@
package com.tangem.feature.wallet.presentation.wallet.utils
import androidx.annotation.DrawableRes
import com.tangem.common.Provider
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
import com.tangem.core.ui.extensions.iconResId
import com.tangem.core.ui.extensions.networkBadgeIconResId
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
import com.tangem.utils.converter.Converter
@ -19,18 +18,6 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
private val clickIntents: WalletClickIntents,
) : Converter<CryptoCurrencyStatus, TokenItemState> {
private val CryptoCurrencyStatus.networkIconResId: Int?
@DrawableRes get() {
// TODO: [REDACTED_JIRA]
return if (currency is CryptoCurrency.Coin) null else R.drawable.img_eth_22
}
private val CryptoCurrencyStatus.tokenIconResId: Int
@DrawableRes get() {
// TODO: [REDACTED_JIRA]
return R.drawable.img_eth_22
}
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
return when (value.value) {
is CryptoCurrencyStatus.Loading -> TokenItemState.Loading(id = value.currency.id.value)
@ -51,8 +38,8 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
id = currency.id.value,
name = currency.name,
tokenIconUrl = currency.iconUrl,
tokenIconResId = this.tokenIconResId,
networkIconResId = this.networkIconResId,
tokenIconResId = currency.iconResId,
networkBadgeIconResId = currency.networkBadgeIconResId,
amount = getFormattedAmount(),
hasPending = value.hasTransactionsInProgress,
tokenOptions = if (isWalletContentHidden) {
@ -63,6 +50,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
priceChange = getPriceChangeConfig(),
)
},
isTestnet = currency.network.isTestnet,
onClick = { clickIntents.onTokenClick(currency) },
)
}
@ -84,8 +72,8 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
id = currency.id.value,
name = currency.name,
tokenIconUrl = currency.iconUrl,
tokenIconResId = this.tokenIconResId,
networkIconResId = this.networkIconResId,
tokenIconResId = currency.iconResId,
networkBadgeIconResId = currency.networkBadgeIconResId,
)
private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeConfig {