diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt
index 7c32f55014..752b3cc492 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt
@@ -1,45 +1,49 @@
package com.tangem.core.ui.extensions
import androidx.annotation.DrawableRes
-import com.tangem.core.ui.R
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.luminance
+import androidx.core.graphics.toColorInt
+import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.domain.tokens.models.CryptoCurrency
+private const val LIGHT_LUMINANCE = 0.5f
+private const val COLOR_HEX_START_INDEX = 2
+private const val COLOR_HEX_END_INDEX = 7
+
/**
- * Retrieves the resource ID for the network badge of a [CryptoCurrency].
+ * Retrieves the resource ID for the network 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.
+ * @return Drawable resource ID for the network.
*/
@get:DrawableRes
-val CryptoCurrency.networkBadgeIconResId: Int?
- get() = when (this) {
- is CryptoCurrency.Coin -> null
- is CryptoCurrency.Token -> getActiveIconRes(network.id.value)
+val CryptoCurrency.networkIconResId: Int
+ get() = getActiveIconRes(network.id.value)
+
+/**
+ * Tries to extract a background color from the contract address of a token.
+ *
+ * @param fallbackColor The color to use as a fallback.
+ * @return The extracted background color or the fallback color if extraction fails or if it is a test network token.
+ */
+fun CryptoCurrency.Token.tryGetBackgroundForTokenIcon(fallbackColor: Color = TangemColorPalette.Black): Color {
+ if (network.isTestnet) return fallbackColor
+
+ return try {
+ val colorHex = "#" + contractAddress.substring(range = COLOR_HEX_START_INDEX..COLOR_HEX_END_INDEX)
+ Color(colorHex.toColorInt())
+ } catch (exception: Exception) {
+ fallbackColor
}
+}
/**
- * Retrieves the resource ID for the icon of a [CryptoCurrency].
+ * Determines the tint color to be used for a token icon based on its background color.
+ * If the icon's background color is light, a dark tint is chosen; otherwise, a light tint is chosen.
*
- * 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.
+ * @param iconBackground The background color of the custom token icon.
+ * @return The tint color to be used for the 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
- }
\ No newline at end of file
+fun getTintForTokenIcon(iconBackground: Color): Color {
+ return if (iconBackground.luminance() > LIGHT_LUMINANCE) TangemColorPalette.Black else TangemColorPalette.White
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt
index 139dc810c6..efe05116e7 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt
@@ -88,6 +88,7 @@ private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors {
}
@Composable
+@ReadOnlyComposable
private fun lightThemeColors(): TangemColors {
return TangemColors(
text = TangemColors.Text(
@@ -135,6 +136,7 @@ private fun lightThemeColors(): TangemColors {
}
@Composable
+@ReadOnlyComposable
private fun darkThemeColors(): TangemColors {
return TangemColors(
text = TangemColors.Text(
diff --git a/core/ui/src/main/res/drawable/ic_custom_token_44.xml b/core/ui/src/main/res/drawable/ic_custom_token_44.xml
new file mode 100644
index 0000000000..b1d31ab0bd
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_custom_token_44.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt
index 0bb96c3d93..29e967a070 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt
@@ -3,14 +3,10 @@ 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.TextReference
-import com.tangem.core.ui.extensions.iconResId
+import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
-import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
-import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
-import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
-import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSkeletonStateConverter.SkeletonModel
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
@@ -38,7 +34,7 @@ internal class TokenDetailsSkeletonStateConverter(
is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token(
networkName = currency.network.standardType.name,
blockchainName = currency.network.name,
- networkIcon = currency.iconResId,
+ networkIcon = currency.networkIconResId,
)
},
),
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt
index 60f3ec11ff..de4226ceca 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt
@@ -8,13 +8,17 @@ import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.consumed
import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
-import com.tangem.feature.wallet.presentation.wallet.state.*
+import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig
+import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig
+import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
+import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.components.*
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState
import kotlinx.collections.immutable.persistentListOf
@@ -87,12 +91,34 @@ internal object WalletPreviewData {
)
}
+ private val coinIconState
+ get() = TokenItemState.IconState.CoinIcon(
+ url = null,
+ fallbackResId = R.drawable.img_polygon_22,
+ isGrayscale = false,
+ )
+
+ private val tokenIconState
+ get() = TokenItemState.IconState.TokenIcon(
+ url = null,
+ networkBadgeIconResId = R.drawable.img_polygon_22,
+ fallbackTint = TangemColorPalette.Black,
+ fallbackBackground = TangemColorPalette.Meadow,
+ isGrayscale = false,
+ )
+
+ private val customTokenIconState
+ get() = TokenItemState.IconState.CustomTokenIcon(
+ tint = TangemColorPalette.Black,
+ background = TangemColorPalette.Meadow,
+ networkBadgeIconResId = R.drawable.img_polygon_22,
+ isGrayscale = false,
+ )
+
val tokenItemVisibleState by lazy {
TokenItemState.Content(
id = UUID.randomUUID().toString(),
- tokenIconUrl = null,
- tokenIconResId = R.drawable.img_polygon_22,
- networkBadgeIconResId = R.drawable.img_polygon_22,
+ icon = coinIconState,
name = "Polygon",
amount = "5,412 MATIC",
hasPending = true,
@@ -103,7 +129,6 @@ internal object WalletPreviewData {
type = PriceChangeConfig.Type.UP,
),
),
- isTestnet = false,
onItemClick = {},
onItemLongClick = {},
)
@@ -112,16 +137,14 @@ internal object WalletPreviewData {
val testnetTokenItemVisibleState by lazy {
tokenItemVisibleState.copy(
name = "Polygon testnet",
- isTestnet = true,
+ icon = tokenIconState.copy(isGrayscale = true),
)
}
val tokenItemHiddenState by lazy {
TokenItemState.Content(
id = UUID.randomUUID().toString(),
- tokenIconUrl = null,
- tokenIconResId = R.drawable.img_polygon_22,
- networkBadgeIconResId = R.drawable.img_polygon_22,
+ icon = tokenIconState,
name = "Polygon",
amount = "5,412 MATIC",
hasPending = true,
@@ -131,7 +154,6 @@ internal object WalletPreviewData {
type = PriceChangeConfig.Type.UP,
),
),
- isTestnet = false,
onItemClick = {},
onItemLongClick = {},
)
@@ -140,11 +162,8 @@ internal object WalletPreviewData {
val tokenItemDragState by lazy {
TokenItemState.Draggable(
id = UUID.randomUUID().toString(),
- tokenIconUrl = null,
- tokenIconResId = R.drawable.img_polygon_22,
- networkBadgeIconResId = R.drawable.img_polygon_22,
+ icon = tokenIconState,
name = "Polygon",
- isTestnet = false,
fiatAmount = "3 172,14 $",
)
}
@@ -152,13 +171,28 @@ internal object WalletPreviewData {
val tokenItemUnreachableState by lazy {
TokenItemState.Unreachable(
id = UUID.randomUUID().toString(),
- tokenIconUrl = null,
- tokenIconResId = R.drawable.img_polygon_22,
- networkBadgeIconResId = R.drawable.img_polygon_22,
+ icon = tokenIconState,
name = "Polygon",
)
}
+ val customTokenItemVisibleState by lazy {
+ tokenItemVisibleState.copy(
+ name = "Polygon custom",
+ icon = customTokenIconState.copy(
+ tint = TangemColorPalette.White,
+ background = TangemColorPalette.Black,
+ ),
+ )
+ }
+
+ val customTestnetTokenItemVisibleState by lazy {
+ tokenItemVisibleState.copy(
+ name = "Polygon custom testnet",
+ icon = customTokenIconState.copy(isGrayscale = true),
+ )
+ }
+
val loadingTokenItemState by lazy { TokenItemState.Loading(id = "Loading#1") }
private const val networksSize = 10
@@ -188,7 +222,6 @@ internal object WalletPreviewData {
tokenItemState = tokenItemDragState.copy(
id = "${group.id}_token_$tokenNumber",
name = "Token $tokenNumber from $networkNumber network",
- networkBadgeIconResId = R.drawable.img_eth_22.takeIf { i != 0 },
),
groupId = group.id,
roundingMode = when {
@@ -299,8 +332,6 @@ internal object WalletPreviewData {
tokenItemVisibleState.copy(
id = "token_1",
name = "Ethereum",
- tokenIconResId = R.drawable.img_eth_22,
- networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),
@@ -308,8 +339,6 @@ internal object WalletPreviewData {
tokenItemVisibleState.copy(
id = "token_2",
name = "Ethereum",
- tokenIconResId = R.drawable.img_eth_22,
- networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),
@@ -317,8 +346,6 @@ internal object WalletPreviewData {
tokenItemVisibleState.copy(
id = "token_3",
name = "Ethereum",
- tokenIconResId = R.drawable.img_eth_22,
- networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),
@@ -326,8 +353,6 @@ internal object WalletPreviewData {
tokenItemVisibleState.copy(
id = "token_4",
name = "Ethereum",
- tokenIconResId = R.drawable.img_eth_22,
- networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),
@@ -336,8 +361,6 @@ internal object WalletPreviewData {
tokenItemVisibleState.copy(
id = "token_5",
name = "Ethereum",
- tokenIconResId = R.drawable.img_eth_22,
- networkBadgeIconResId = null,
amount = "1,89340821 ETH",
),
),
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt
index 6169aa9a8c..18cc14cbe1 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt
@@ -17,16 +17,14 @@ import androidx.constraintlayout.compose.ConstrainedLayoutReference
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.ConstraintLayoutScope
import androidx.constraintlayout.compose.Dimension
-import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.component.token.TokenCryptoInfoBlock
import com.tangem.feature.wallet.presentation.common.component.token.TokenFiatInfoBlock
-import com.tangem.feature.wallet.presentation.common.component.token.TokenIcon
+import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import org.burnoutcrew.reorderable.ReorderableLazyListState
-// TODO: Add custom token state: [REDACTED_JIRA]
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun TokenItem(
@@ -90,7 +88,7 @@ private inline fun BaseContainer(
) {
ConstraintLayout(
modifier = Modifier
- .fillMaxSize()
+ .fillMaxWidth()
.padding(
horizontal = TangemTheme.dimens.spacing14,
vertical = TangemTheme.dimens.spacing14,
@@ -111,7 +109,6 @@ private fun Modifier.constrainAsOptionsItem(scope: ConstraintLayoutScope, ref: C
}
// region preview
-
@Preview
@Composable
private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) {
@@ -136,6 +133,8 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider ContentIcon(content = state, modifier = modifier)
- is TokenItemState.Loading -> LoadingIcon(modifier = modifier)
- is TokenItemState.Locked -> LockedIcon(modifier = modifier)
- }
-}
-
-@Composable
-private fun ContentIcon(content: TokenItemState.ContentState, modifier: Modifier = Modifier) {
- BaseContainer(modifier = modifier) {
- val isTestnet = when (content) {
- is TokenItemState.Content -> content.isTestnet
- is TokenItemState.Draggable -> content.isTestnet
- is TokenItemState.Unreachable -> false
- }
-
- val colorFilter = remember(isTestnet) {
- if (isTestnet) {
- ColorFilter.colorMatrix(
- colorMatrix = ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) },
- )
- } else {
- null
- }
- }
-
- Icon(
- content = content,
- colorFilter = colorFilter,
- modifier = Modifier.align(Alignment.BottomStart),
- )
-
- NetworkBadge(
- iconResId = content.networkBadgeIconResId,
- colorFilter = colorFilter,
- modifier = Modifier.align(Alignment.TopEnd),
- )
- }
-}
-
-@Composable
-private fun Icon(content: TokenItemState.ContentState, colorFilter: ColorFilter?, modifier: Modifier = Modifier) {
- val iconUrl = content.tokenIconUrl
- val iconData: Any = remember(iconUrl) {
- if (iconUrl.isNullOrEmpty()) content.tokenIconResId else iconUrl
- }
-
- SubcomposeAsyncImage(
- modifier = modifier.iconSize(),
- model = ImageRequest.Builder(context = LocalContext.current)
- .data(data = iconData)
- .placeholder(drawableResId = content.tokenIconResId)
- .error(drawableResId = content.tokenIconResId)
- .fallback(drawableResId = content.tokenIconResId)
- .crossfade(enable = true)
- .build(),
- colorFilter = colorFilter,
- contentDescription = null,
- )
-}
-
-@Composable
-private fun BoxScope.NetworkBadge(
- @DrawableRes iconResId: Int?,
- colorFilter: ColorFilter?,
- modifier: Modifier = Modifier,
-) {
- AnimatedVisibility(
- visible = iconResId != null,
- modifier = modifier
- .size(TangemTheme.dimens.size18)
- .background(color = TangemTheme.colors.background.primary, shape = CircleShape),
- ) {
- if (iconResId == null) return@AnimatedVisibility
-
- Image(
- modifier = Modifier
- .padding(all = TangemTheme.dimens.spacing2)
- .align(Alignment.Center),
- painter = painterResource(id = iconResId),
- colorFilter = colorFilter,
- contentDescription = null,
- )
- }
-}
-
-@Composable
-private fun LoadingIcon(modifier: Modifier = Modifier) {
- BaseContainer(modifier) {
- CircleShimmer(
- modifier = Modifier
- .iconSize()
- .align(alignment = Alignment.BottomStart),
- )
- }
-}
-
-@Composable
-private fun LockedIcon(modifier: Modifier = Modifier) {
- BaseContainer(modifier) {
- Box(
- modifier = Modifier
- .iconSize()
- .align(Alignment.BottomStart),
- ) {
- Box(
- modifier = Modifier
- .matchParentSize()
- .background(color = TangemTheme.colors.background.secondary, shape = CircleShape),
- )
- }
- }
-}
-
-@Composable
-private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) {
- Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content)
-}
-
-private fun Modifier.iconSize(): Modifier = composed {
- return@composed this.size(size = TangemTheme.dimens.size36)
-}
\ No newline at end of file
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt
new file mode 100644
index 0000000000..c51cb1e957
--- /dev/null
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt
@@ -0,0 +1,129 @@
+package com.tangem.feature.wallet.presentation.common.component.token.icon
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+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.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import coil.compose.SubcomposeAsyncImage
+import coil.request.ImageRequest
+import com.tangem.feature.wallet.impl.R
+import com.tangem.feature.wallet.presentation.common.state.TokenItemState
+
+@Composable
+internal fun ContentIcon(icon: TokenItemState.IconState, colorFilter: ColorFilter?, modifier: Modifier = Modifier) {
+ when (icon) {
+ is TokenItemState.IconState.CoinIcon -> CoinIcon(
+ modifier = modifier,
+ url = icon.url,
+ fallbackResId = icon.fallbackResId,
+ colorFilter = colorFilter,
+ )
+ is TokenItemState.IconState.TokenIcon -> TokenIcon(
+ modifier = modifier,
+ url = icon.url,
+ colorFilter = colorFilter,
+ errorIcon = {
+ CustomTokenIcon(
+ modifier = modifier,
+ tint = icon.fallbackTint,
+ background = icon.fallbackBackground,
+ )
+ },
+ )
+ is TokenItemState.IconState.CustomTokenIcon -> CustomTokenIcon(
+ modifier = modifier,
+ tint = icon.tint,
+ background = icon.background,
+ )
+ }
+}
+
+@Composable
+private fun CoinIcon(
+ url: String?,
+ @DrawableRes fallbackResId: Int,
+ colorFilter: ColorFilter?,
+ modifier: Modifier = Modifier,
+) {
+ val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url
+
+ DefaultCurrencyIcon(
+ modifier = modifier,
+ iconData = iconData,
+ errorIcon = {
+ Image(
+ painter = painterResource(id = fallbackResId),
+ colorFilter = colorFilter,
+ contentDescription = null,
+ )
+ },
+ colorFilter = colorFilter,
+ )
+}
+
+@Composable
+private fun TokenIcon(
+ url: String?,
+ colorFilter: ColorFilter?,
+ errorIcon: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ if (url == null) {
+ errorIcon()
+ } else {
+ DefaultCurrencyIcon(
+ modifier = modifier,
+ iconData = url,
+ errorIcon = errorIcon,
+ colorFilter = colorFilter,
+ )
+ }
+}
+
+@Composable
+private fun CustomTokenIcon(tint: Color, background: Color, modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier
+ .background(
+ color = background,
+ shape = CircleShape,
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ modifier = Modifier.matchParentSize(),
+ painter = painterResource(id = R.drawable.ic_custom_token_44),
+ tint = tint,
+ contentDescription = null,
+ )
+ }
+}
+
+@Composable
+private inline fun DefaultCurrencyIcon(
+ iconData: Any,
+ colorFilter: ColorFilter?,
+ crossinline errorIcon: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ SubcomposeAsyncImage(
+ modifier = modifier,
+ model = ImageRequest.Builder(context = LocalContext.current)
+ .data(iconData)
+ .crossfade(enable = true)
+ .build(),
+ loading = { LoadingIcon() },
+ error = { errorIcon() },
+ colorFilter = colorFilter,
+ contentDescription = null,
+ )
+}
\ No newline at end of file
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt
new file mode 100644
index 0000000000..8bc2dbb218
--- /dev/null
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt
@@ -0,0 +1,57 @@
+package com.tangem.feature.wallet.presentation.common.component.token.icon
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.res.painterResource
+import com.tangem.core.ui.res.TangemTheme
+
+@Composable
+internal fun NetworkBadge(@DrawableRes iconResId: Int, colorFilter: ColorFilter?, modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier
+ .size(TangemTheme.dimens.size18)
+ .background(
+ color = TangemTheme.colors.background.primary,
+ shape = CircleShape,
+ ),
+ ) {
+ Image(
+ modifier = Modifier
+ .padding(all = TangemTheme.dimens.spacing2)
+ .matchParentSize(),
+ painter = painterResource(id = iconResId),
+ colorFilter = colorFilter,
+ contentDescription = null,
+ )
+ }
+}
+
+@Composable
+internal fun CustomBadge(modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier
+ .size(TangemTheme.dimens.size12)
+ .background(
+ color = TangemTheme.colors.background.primary,
+ shape = CircleShape,
+ ),
+ ) {
+ Box(
+ modifier = Modifier
+ .padding(all = TangemTheme.dimens.spacing2)
+ .matchParentSize()
+ .background(
+ color = TangemTheme.colors.icon.informative,
+ shape = CircleShape,
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt
new file mode 100644
index 0000000000..c227566471
--- /dev/null
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt
@@ -0,0 +1,95 @@
+package com.tangem.feature.wallet.presentation.common.component.token.icon
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.ColorMatrix
+import com.tangem.core.ui.components.CircleShimmer
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.feature.wallet.presentation.common.state.TokenItemState
+
+private const val GRAY_SCALE_SATURATION = 0f
+
+@Composable
+internal fun TokenIcon(state: TokenItemState, modifier: Modifier = Modifier) {
+ BaseContainer(modifier = modifier) {
+ val iconModifier = Modifier
+ .align(Alignment.Center)
+ .size(TangemTheme.dimens.size36)
+
+ when (state) {
+ is TokenItemState.Loading -> LoadingIcon(modifier = iconModifier)
+ is TokenItemState.Locked -> LockedIcon(modifier = iconModifier)
+ is TokenItemState.ContentState -> ContentIconContainer(
+ modifier = iconModifier,
+ icon = state.icon,
+ )
+ }
+ }
+}
+
+@Composable
+internal fun LoadingIcon(modifier: Modifier = Modifier) {
+ CircleShimmer(modifier = modifier)
+}
+
+@Composable
+private fun LockedIcon(modifier: Modifier = Modifier) {
+ Box(modifier = modifier) {
+ Box(
+ modifier = Modifier
+ .matchParentSize()
+ .background(
+ color = TangemTheme.colors.background.secondary,
+ shape = CircleShape,
+ ),
+ )
+ }
+}
+
+@Composable
+private fun BoxScope.ContentIconContainer(icon: TokenItemState.IconState, modifier: Modifier = Modifier) {
+ val networkBadgeOffset = TangemTheme.dimens.spacing4
+ val colorFilter = remember(icon.isGrayscale) {
+ if (icon.isGrayscale) {
+ ColorFilter.colorMatrix(
+ colorMatrix = ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) },
+ )
+ } else {
+ null
+ }
+ }
+
+ ContentIcon(
+ modifier = modifier,
+ icon = icon,
+ colorFilter = colorFilter,
+ )
+
+ if (icon.networkBadgeIconResId != null) {
+ NetworkBadge(
+ modifier = Modifier
+ .offset(x = networkBadgeOffset, y = -networkBadgeOffset)
+ .align(Alignment.TopEnd),
+ iconResId = requireNotNull(icon.networkBadgeIconResId),
+ colorFilter = colorFilter,
+ )
+ }
+
+ if (icon is TokenItemState.IconState.CustomTokenIcon) {
+ CustomBadge(modifier = Modifier.align(Alignment.BottomEnd))
+ }
+}
+
+@Composable
+private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) {
+ Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content)
+}
\ No newline at end of file
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt
index 8310f134d5..16e58f8a87 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt
@@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.common.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
/** Token item state */
@@ -18,80 +19,120 @@ internal sealed interface TokenItemState {
data class Locked(override val id: String) : TokenItemState
/** Content state */
- sealed class ContentState(
- override val id: String,
- open val tokenIconUrl: String?,
- @DrawableRes open val tokenIconResId: Int,
- @DrawableRes open val networkBadgeIconResId: Int?,
- open val name: String,
- ) : TokenItemState
+ sealed class ContentState : TokenItemState {
+
+ abstract val icon: IconState
+ abstract val name: String
+ }
/**
* Content token state
*
* @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 icon token icon state
* @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 onItemClick callback which will be called when an item is clicked
* @property onItemLongClick callback which will be called when an item is long clicked
*/
data class Content(
override val id: String,
- override val tokenIconUrl: String?,
- @DrawableRes override val tokenIconResId: Int,
- @DrawableRes override val networkBadgeIconResId: Int?,
+ override val icon: IconState,
override val name: String,
val amount: String,
val hasPending: Boolean,
val tokenOptions: TokenOptionsState,
- val isTestnet: Boolean,
val onItemClick: () -> Unit,
val onItemLongClick: () -> Unit,
- ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name)
+ ) : ContentState()
/**
* Draggable token state
*
* @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 icon token icon state
* @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,
- override val tokenIconUrl: String?,
- @DrawableRes override val tokenIconResId: Int,
- @DrawableRes override val networkBadgeIconResId: Int?,
+ override val icon: IconState,
override val name: String,
val fiatAmount: String,
- val isTestnet: Boolean,
- ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name)
+ ) : ContentState()
/**
* Unreachable token state
*
* @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 icon token icon state
* @property name token name
*/
data class Unreachable(
override val id: String,
- override val tokenIconUrl: String?,
- @DrawableRes override val tokenIconResId: Int,
- @DrawableRes override val networkBadgeIconResId: Int?,
+ override val icon: IconState,
override val name: String,
- ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name)
+ ) : ContentState()
+
+ /**
+ * Represents the various states an icon can be in.
+ */
+ @Immutable
+ sealed class IconState {
+
+ abstract val networkBadgeIconResId: Int?
+ abstract val isGrayscale: Boolean
+
+ /**
+ * Represents a coin icon.
+ *
+ * @property url The URL where the coin icon can be fetched from. May be `null` if not found.
+ * @property fallbackResId The drawable resource ID to be used as a fallback if the URL is not available.
+ * @property isGrayscale Specifies whether to show the icon in grayscale.
+ */
+ data class CoinIcon(
+ val url: String?,
+ @DrawableRes val fallbackResId: Int,
+ override val isGrayscale: Boolean,
+ ) : IconState() {
+
+ override val networkBadgeIconResId: Int? = null
+ }
+
+ /**
+ * Represents a token icon.
+ *
+ * @property url The URL where the token icon can be fetched from. May be `null` if not found.
+ * @property networkBadgeIconResId The drawable resource ID for the network badge.
+ * @property isGrayscale Specifies whether to show the icon in grayscale.
+ * @property fallbackTint The color to be used for tinting the fallback icon.
+ * @property fallbackBackground The background color to be used for the fallback icon.
+ */
+ data class TokenIcon(
+ val url: String?,
+ @DrawableRes override val networkBadgeIconResId: Int,
+ override val isGrayscale: Boolean,
+ val fallbackTint: Color,
+ val fallbackBackground: Color,
+ ) : IconState()
+
+ /**
+ * Represents a custom token icon.
+ *
+ * @property tint The color to be used for tinting the icon.
+ * @property background The background color to be used for the icon.
+ * @property networkBadgeIconResId The drawable resource ID for the network badge.
+ * @property isGrayscale Specifies whether to show the icon in grayscale.
+ */
+ data class CustomTokenIcon(
+ val tint: Color,
+ val background: Color,
+ @DrawableRes override val networkBadgeIconResId: Int,
+ override val isGrayscale: Boolean,
+ ) : IconState()
+ }
/** Token options state */
@Immutable
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt
new file mode 100644
index 0000000000..6f8c7c9b05
--- /dev/null
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt
@@ -0,0 +1,48 @@
+package com.tangem.feature.wallet.presentation.common.utils
+
+import com.tangem.core.ui.extensions.getTintForTokenIcon
+import com.tangem.core.ui.extensions.networkIconResId
+import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
+import com.tangem.domain.tokens.models.CryptoCurrency
+import com.tangem.feature.wallet.presentation.common.state.TokenItemState
+import com.tangem.utils.converter.Converter
+
+internal class CryptoCurrencyToIconStateConverter : Converter {
+
+ override fun convert(value: CryptoCurrency): TokenItemState.IconState {
+ return when (value) {
+ is CryptoCurrency.Coin -> getIconStateForCoin(value)
+ is CryptoCurrency.Token -> getIconStateForToken(value)
+ }
+ }
+
+ private fun getIconStateForCoin(coin: CryptoCurrency.Coin): TokenItemState.IconState.CoinIcon {
+ return TokenItemState.IconState.CoinIcon(
+ url = coin.iconUrl,
+ fallbackResId = coin.networkIconResId,
+ isGrayscale = coin.network.isTestnet,
+ )
+ }
+
+ private fun getIconStateForToken(token: CryptoCurrency.Token): TokenItemState.IconState {
+ val background = token.tryGetBackgroundForTokenIcon()
+ val tint = getTintForTokenIcon(background)
+
+ return if (token.isCustom) {
+ TokenItemState.IconState.CustomTokenIcon(
+ tint = tint,
+ background = background,
+ networkBadgeIconResId = token.networkIconResId,
+ isGrayscale = token.network.isTestnet,
+ )
+ } else {
+ TokenItemState.IconState.TokenIcon(
+ url = token.iconUrl,
+ networkBadgeIconResId = token.networkIconResId,
+ isGrayscale = token.network.isTestnet,
+ fallbackTint = tint,
+ fallbackBackground = background,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt
index 238ff49a18..4cd2ee53d0 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt
@@ -1,7 +1,9 @@
package com.tangem.feature.wallet.presentation.organizetokens
import androidx.activity.compose.BackHandler
-import androidx.compose.animation.core.*
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@@ -37,7 +39,10 @@ import com.tangem.feature.wallet.presentation.common.component.TokenItem
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
-import org.burnoutcrew.reorderable.*
+import org.burnoutcrew.reorderable.ReorderableItem
+import org.burnoutcrew.reorderable.ReorderableLazyListState
+import org.burnoutcrew.reorderable.rememberReorderableLazyListState
+import org.burnoutcrew.reorderable.reorderable
@Composable
internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier = Modifier) {
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt
index 11f4747c2b..9bad7941b4 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt
@@ -1,12 +1,11 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
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.feature.wallet.presentation.common.state.TokenItemState
+import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId
@@ -16,6 +15,8 @@ internal class CryptoCurrencyToDraggableItemConverter(
private val appCurrencyProvider: Provider,
) : Converter {
+ private val iconStateConverter = CryptoCurrencyToIconStateConverter()
+
override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token {
return createDraggableToken(value, appCurrencyProvider())
}
@@ -44,12 +45,9 @@ internal class CryptoCurrencyToDraggableItemConverter(
return TokenItemState.Draggable(
id = getTokenItemId(currency.id),
- tokenIconUrl = currency.iconUrl,
- tokenIconResId = currencyStatus.currency.iconResId,
- networkBadgeIconResId = currencyStatus.currency.networkBadgeIconResId,
+ icon = iconStateConverter.convert(currency),
name = currency.name,
fiatAmount = getFormattedFiatAmount(currencyStatus, appCurrency),
- isTestnet = currencyStatus.currency.network.isTestnet,
)
}
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt
index b19dcf646a..02b175aad7 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt
@@ -2,12 +2,11 @@ package com.tangem.feature.wallet.presentation.wallet.utils
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.feature.wallet.presentation.common.state.TokenItemState
+import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
@@ -18,6 +17,8 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
private val clickIntents: WalletClickIntents,
) : Converter {
+ private val iconStateConverter = CryptoCurrencyToIconStateConverter()
+
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
return when (value.value) {
is CryptoCurrencyStatus.Loading -> TokenItemState.Loading(id = value.currency.id.value)
@@ -37,9 +38,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
return TokenItemState.Content(
id = currency.id.value,
name = currency.name,
- tokenIconUrl = currency.iconUrl,
- tokenIconResId = currency.iconResId,
- networkBadgeIconResId = currency.networkBadgeIconResId,
+ icon = iconStateConverter.convert(currency),
amount = getFormattedAmount(),
hasPending = value.hasCurrentNetworkTransactions,
tokenOptions = if (isWalletContentHidden) {
@@ -50,7 +49,6 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
config = getPriceChangeConfig(),
)
},
- isTestnet = currency.network.isTestnet,
onItemClick = { clickIntents.onTokenItemClick(currency) },
onItemLongClick = { clickIntents.onTokenItemLongClick(currency) },
)
@@ -72,9 +70,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable(
id = currency.id.value,
name = currency.name,
- tokenIconUrl = currency.iconUrl,
- tokenIconResId = currency.iconResId,
- networkBadgeIconResId = currency.networkBadgeIconResId,
+ icon = iconStateConverter.convert(currency),
)
private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeConfig {