From 324f365771a854829bf4778e8cd0f61c842033a3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Oct 2023 13:21:24 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../middlewares/MultiWalletMiddleware.kt | 25 +-- .../domain/tokens/model/CryptoCurrency.kt | 3 +- .../com/tangem/domain/tokens/model/Network.kt | 25 +-- .../navigation/TokenDetailsArguments.kt | 33 ---- .../navigation/TokenDetailsRouter.kt | 2 +- .../tokendetails/TokenDetailsPreviewData.kt | 20 ++- .../tokendetails/state/TokenInfoBlockState.kt | 30 +++- .../factory/TokenDetailsIconStateConverter.kt | 47 ++++++ .../TokenDetailsSkeletonStateConverter.kt | 30 ++-- .../state/factory/TokenDetailsStateFactory.kt | 15 +- .../tokendetails/ui/components/TokenIcon.kt | 146 ++++++++++++++++++ .../ui/components/TokenInfoBlock.kt | 30 ++-- .../viewmodels/TokenDetailsViewModel.kt | 28 +--- .../router/DefaultWalletRouter.kt | 20 +-- 14 files changed, 310 insertions(+), 144 deletions(-) delete mode 100644 features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 6a76d4ef38..eb198a07f9 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -7,10 +7,7 @@ import com.tangem.common.flatMap import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.extensions.networkIconResId -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.tokendetails.navigation.TokenDetailsArguments import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken @@ -20,7 +17,6 @@ import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError -import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.converters.CryptoCurrencyConverter import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState @@ -44,7 +40,7 @@ class MultiWalletMiddleware { is WalletAction.MultiWallet.SelectWallet -> { if (action.currency != null) { val bundle = bundleOf( - TokenDetailsRouter.TOKEN_DETAILS_ARGS to createTokenDetailsArgument(action.currency), + TokenDetailsRouter.CRYPTO_CURRENCY_KEY to cryptoCurrencyConverter.convert(action.currency), ) store.dispatch(NavigationAction.NavigateTo(screen = AppScreen.WalletDetails, bundle = bundle)) } @@ -133,23 +129,4 @@ class MultiWalletMiddleware { store.state.globalState.tapWalletManager.loadData(updatedUserWallet, refresh = true) } } - - private fun createTokenDetailsArgument(currency: Currency): TokenDetailsArguments { - val cryptoCurrency = cryptoCurrencyConverter.convert(currency) - return TokenDetailsArguments( - currencyId = cryptoCurrency.id, - currencyName = cryptoCurrency.name, - currencySymbol = cryptoCurrency.symbol, - iconUrl = cryptoCurrency.iconUrl, - coinType = when (cryptoCurrency) { - is CryptoCurrency.Coin -> TokenDetailsArguments.CoinType.Native - is CryptoCurrency.Token -> TokenDetailsArguments.CoinType.Token( - isCustom = cryptoCurrency.isCustom, - standardName = cryptoCurrency.network.standardType.name, - networkName = cryptoCurrency.network.name, - networkIcon = cryptoCurrency.networkIconResId, - ) - }, - ) - } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt index 161c9cb1cc..265f94094c 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt @@ -14,7 +14,8 @@ import kotlinx.parcelize.Parcelize * @property iconUrl Optional URL of the cryptocurrency icon. `null` if not found. * @property isCustom Indicates whether the currency is a custom user-added currency or not. */ -sealed class CryptoCurrency { +@Parcelize +sealed class CryptoCurrency : Parcelable { abstract val id: ID abstract val network: Network diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt index 03bfac5ee6..2bc545ba77 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt @@ -1,5 +1,8 @@ package com.tangem.domain.tokens.model +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + /** * Represents a blockchain network, identified by a unique ID, a human-readable name, and its standard type. * @@ -13,13 +16,14 @@ package com.tangem.domain.tokens.model * @property isTestnet Indicates whether the network is a test network or a main network. * @property standardType The type of blockchain standard the network adheres to. */ +@Parcelize data class Network( val id: ID, val name: String, val derivationPath: DerivationPath, val isTestnet: Boolean, val standardType: StandardType, -) { +) : Parcelable { init { require(name.isNotBlank()) { "Network name must not be blank" } @@ -31,7 +35,8 @@ data class Network( * @property value The string representation of the network ID. */ @JvmInline - value class ID(val value: String) { + @Parcelize + value class ID(val value: String) : Parcelable { init { require(value.isNotBlank()) { "Network ID must not be blank" } @@ -44,7 +49,8 @@ data class Network( * This class represents such paths in a generic manner, allowing for predefined card-based paths, * custom paths, or even no derivation path at all. */ - sealed class DerivationPath { + @Parcelize + sealed class DerivationPath : Parcelable { /** The actual derivation path value, if any. */ abstract val value: String? @@ -67,7 +73,7 @@ data class Network( * Represents a lack of derivation path. */ object None : DerivationPath() { - override val value: String? = null + override val value: String? get() = null } } @@ -80,27 +86,28 @@ data class Network( * * @property name The human-readable name of the standard type. */ - sealed class StandardType { + @Parcelize + sealed class StandardType : Parcelable { abstract val name: String /** Represents the ERC20 token standard, common on the Ethereum network. */ object ERC20 : StandardType() { - override val name: String = "ERC20" + override val name: String get() = "ERC20" } /** Represents the TRC20 token standard, common on the TRON network. */ object TRC20 : StandardType() { - override val name: String = "TRC20" + override val name: String get() = "TRC20" } /** Represents the BEP20 token standard, common on the Binance Smart Chain network. */ object BEP20 : StandardType() { - override val name: String = "BEP20" + override val name: String get() = "BEP20" } /** Represents the BEP2 token standard, common on the Binance Chain network. */ object BEP2 : StandardType() { - override val name: String = "BEP2" + override val name: String get() = "BEP2" } /** Represents a network that does not adhere to a predefined standard type. */ diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt deleted file mode 100644 index 5238d98973..0000000000 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.features.tokendetails.navigation - -import android.os.Parcelable -import androidx.annotation.DrawableRes -import com.tangem.domain.tokens.model.CryptoCurrency -import kotlinx.parcelize.Parcelize - -@Parcelize -data class TokenDetailsArguments( - val currencyId: CryptoCurrency.ID, - val currencyName: String, - val currencySymbol: String, - val iconUrl: String?, - val coinType: CoinType, -) : Parcelable { - - @Parcelize - sealed class CoinType : Parcelable { - object Native : CoinType() - - /** - * @param isCustom - Indicates whether the currency is a custom user-added currency or not. - * @param standardName - token standard. Samples: ERC20, BEP20, BEP2, TRC20 and etc. - * @param networkName - token's blockchain name. Ethereum, Tron and etc. - */ - data class Token( - val isCustom: Boolean, - val standardName: String, - val networkName: String, - @DrawableRes val networkIcon: Int, - ) : CoinType() - } -} \ No newline at end of file diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt index b0130e64cf..da5bc7571f 100644 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt @@ -7,6 +7,6 @@ interface TokenDetailsRouter { fun getEntryFragment(): Fragment companion object { - const val TOKEN_DETAILS_ARGS = "token_details_args" + const val CRYPTO_CURRENCY_KEY = "token_details_crypto_currency" } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 664c3fd0e1..6db94b8e34 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails +import androidx.compose.ui.graphics.Color import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference @@ -32,12 +33,21 @@ internal object TokenDetailsPreviewData { val tokenInfoBlockStateWithLongNameInMainCurrency = TokenInfoBlockState( name = "Stellar (XLM) with long name test", - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + iconState = TokenInfoBlockState.IconState.CoinIcon( + url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + fallbackResId = R.drawable.img_stellar_22, + isGrayscale = false, + ), currency = TokenInfoBlockState.Currency.Native, ) val tokenInfoBlockStateWithLongName = TokenInfoBlockState( name = "Tether (USDT) with long name test", - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + iconState = TokenInfoBlockState.IconState.TokenIcon( + url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + fallbackTint = Color.Cyan, + fallbackBackground = Color.Blue, + isGrayscale = false, + ), currency = TokenInfoBlockState.Currency.Token( standardName = "ERC20", networkIcon = R.drawable.img_eth_22, @@ -47,7 +57,11 @@ internal object TokenDetailsPreviewData { val tokenInfoBlockState = TokenInfoBlockState( name = "Tether USDT", - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png", + iconState = TokenInfoBlockState.IconState.CustomTokenIcon( + tint = Color.Green, + background = Color.Magenta, + isGrayscale = true, + ), currency = TokenInfoBlockState.Currency.Token( standardName = "ERC20", networkIcon = R.drawable.img_eth_22, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt index 10bdfe2821..7c1e1eda2b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt @@ -1,12 +1,15 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color internal data class TokenInfoBlockState( val name: String, - val iconUrl: String?, + val iconState: IconState, val currency: Currency, ) { + @Immutable sealed class Currency { object Native : Currency() @@ -21,4 +24,29 @@ internal data class TokenInfoBlockState( @DrawableRes val networkIcon: Int, ) : Currency() } + + @Immutable + sealed class IconState { + + abstract val isGrayscale: Boolean + + data class CoinIcon( + val url: String?, + @DrawableRes val fallbackResId: Int, + override val isGrayscale: Boolean, + ) : IconState() + + data class TokenIcon( + val url: String?, + val fallbackTint: Color, + val fallbackBackground: Color, + override val isGrayscale: Boolean, + ) : IconState() + + data class CustomTokenIcon( + val tint: Color, + val background: Color, + override val isGrayscale: Boolean, + ) : IconState() + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt new file mode 100644 index 0000000000..ebd12ff9b0 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt @@ -0,0 +1,47 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +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.model.CryptoCurrency +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState +import com.tangem.utils.converter.Converter + +internal class TokenDetailsIconStateConverter : Converter { + + override fun convert(value: CryptoCurrency): TokenInfoBlockState.IconState { + return when (value) { + is CryptoCurrency.Coin -> getIconStateForCoin(value) + is CryptoCurrency.Token -> getIconStateForToken(value) + } + } + + private fun getIconStateForCoin(coin: CryptoCurrency.Coin): TokenInfoBlockState.IconState.CoinIcon { + return TokenInfoBlockState.IconState.CoinIcon( + url = coin.iconUrl, + fallbackResId = coin.networkIconResId, + isGrayscale = coin.network.isTestnet, + ) + } + + private fun getIconStateForToken(token: CryptoCurrency.Token): TokenInfoBlockState.IconState { + val isGrayscale = token.network.isTestnet + val background = token.tryGetBackgroundForTokenIcon(isGrayscale) + val tint = getTintForTokenIcon(background) + + return if (token.isCustom) { + TokenInfoBlockState.IconState.CustomTokenIcon( + tint = tint, + background = background, + isGrayscale = isGrayscale, + ) + } else { + TokenInfoBlockState.IconState.TokenIcon( + url = token.iconUrl, + isGrayscale = isGrayscale, + fallbackTint = tint, + fallbackBackground = background, + ) + } + } +} \ No newline at end of file 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 e828c9a2be..6fa5307dde 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,13 +3,14 @@ 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.networkIconResId import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R -import com.tangem.features.tokendetails.navigation.TokenDetailsArguments import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -17,29 +18,30 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, -) : Converter { +) : Converter { - override fun convert(value: TokenDetailsArguments): TokenDetailsState { - val coinType = value.coinType + private val iconStateConverter by lazy { TokenDetailsIconStateConverter() } + + override fun convert(value: CryptoCurrency): TokenDetailsState { return TokenDetailsState( topAppBarConfig = TokenDetailsTopAppBarConfig( onBackClick = clickIntents::onBackClick, tokenDetailsAppBarMenuConfig = createMenu(), ), tokenInfoBlockState = TokenInfoBlockState( - name = value.currencyName, - iconUrl = value.iconUrl, - currency = when (coinType) { - TokenDetailsArguments.CoinType.Native -> TokenInfoBlockState.Currency.Native - is TokenDetailsArguments.CoinType.Token -> TokenInfoBlockState.Currency.Token( - standardName = coinType.networkName, - networkName = coinType.networkName, - networkIcon = coinType.networkIcon, + name = value.name, + iconState = iconStateConverter.convert(value), + currency = when (value) { + is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native + is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token( + standardName = value.network.standardType.name, + networkName = value.network.name, + networkIcon = value.networkIconResId, ) }, ), tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading(actionButtons = createButtons()), - marketPriceBlockState = MarketPriceBlockState.Loading(value.currencySymbol), + marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol), notifications = persistentListOf(), pendingTxs = persistentListOf(), txHistoryState = TxHistoryState.Content( @@ -51,7 +53,7 @@ internal class TokenDetailsSkeletonStateConverter( pullToRefreshConfig = createPullToRefresh(), bottomSheetConfig = null, isBalanceHidden = true, - isCustomToken = coinType is TokenDetailsArguments.CoinType.Token && coinType.isCustom, + isCustomToken = value.isCustom, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 52ef44ac9c..bf1c5a2ad0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -22,15 +22,14 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents -import com.tangem.features.tokendetails.navigation.TokenDetailsArguments import kotlinx.coroutines.flow.Flow internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, private val clickIntents: TokenDetailsClickIntents, - currencySymbolProvider: Provider, - currencyDecimalsProvider: Provider, + symbol: String, + decimals: Int, ) { private val skeletonStateConverter by lazy { @@ -45,8 +44,8 @@ internal class TokenDetailsStateFactory( TokenDetailsLoadedBalanceConverter( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, - symbol = currencySymbolProvider(), - decimals = currencyDecimalsProvider(), + symbol = symbol, + decimals = decimals, ) } @@ -65,8 +64,8 @@ internal class TokenDetailsStateFactory( TokenDetailsLoadedTxHistoryConverter( currentStateProvider = currentStateProvider, clickIntents = clickIntents, - symbol = currencySymbolProvider(), - decimals = currencyDecimalsProvider(), + symbol = symbol, + decimals = decimals, ) } @@ -76,7 +75,7 @@ internal class TokenDetailsStateFactory( ) } - fun getInitialState(screenArgument: TokenDetailsArguments): TokenDetailsState { + fun getInitialState(screenArgument: CryptoCurrency): TokenDetailsState { return skeletonStateConverter.convert(value = screenArgument) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt new file mode 100644 index 0000000000..a210927b5b --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt @@ -0,0 +1,146 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +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.core.ui.components.CircleShimmer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState +import com.tangem.features.tokendetails.impl.R + +@Composable +internal fun CurrencyIcon( + icon: TokenInfoBlockState.IconState, + alpha: Float, + colorFilter: ColorFilter?, + modifier: Modifier = Modifier, +) { + when (icon) { + is TokenInfoBlockState.IconState.CoinIcon -> CoinIcon( + modifier = modifier, + url = icon.url, + fallbackResId = icon.fallbackResId, + alpha = alpha, + colorFilter = colorFilter, + ) + is TokenInfoBlockState.IconState.TokenIcon -> TokenIcon( + modifier = modifier, + url = icon.url, + alpha = alpha, + colorFilter = colorFilter, + errorIcon = { + CustomTokenIcon( + modifier = modifier, + tint = icon.fallbackTint, + background = icon.fallbackBackground, + alpha = alpha, + ) + }, + ) + is TokenInfoBlockState.IconState.CustomTokenIcon -> CustomTokenIcon( + modifier = modifier, + tint = icon.tint, + background = icon.background, + alpha = alpha, + ) + } +} + +@Composable +private fun CoinIcon( + url: String?, + @DrawableRes fallbackResId: Int, + alpha: Float, + 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), + alpha = alpha, + colorFilter = colorFilter, + contentDescription = null, + ) + }, + alpha = alpha, + colorFilter = colorFilter, + ) +} + +@Composable +private fun TokenIcon( + url: String?, + alpha: Float, + colorFilter: ColorFilter?, + errorIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + if (url == null) { + errorIcon() + } else { + DefaultCurrencyIcon( + modifier = modifier, + iconData = url, + errorIcon = errorIcon, + alpha = alpha, + colorFilter = colorFilter, + ) + } +} + +@Composable +private fun CustomTokenIcon(tint: Color, background: Color, alpha: Float, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .background( + color = background.copy(alpha = alpha), + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.matchParentSize(), + painter = painterResource(id = R.drawable.ic_custom_token_44), + tint = tint.copy(alpha = alpha), + contentDescription = null, + ) + } +} + +@Composable +private inline fun DefaultCurrencyIcon( + iconData: Any, + alpha: Float, + 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 = { CircleShimmer() }, + error = { errorIcon() }, + alpha = alpha, + colorFilter = colorFilter, + contentDescription = null, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt index a4dd5c2b9e..4d149f38c8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt @@ -1,6 +1,5 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Text @@ -10,19 +9,23 @@ 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.platform.LocalInspectionMode +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import coil.compose.rememberAsyncImagePainter import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState import com.tangem.features.tokendetails.impl.R +private const val GRAY_SCALE_SATURATION = 0f +private const val GRAY_SCALE_ALPHA = 0.4f +private const val NORMAL_ALPHA = 1f + @Composable internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Modifier) { Row(modifier = modifier.fillMaxWidth()) { @@ -37,16 +40,18 @@ internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Mod NetworkInfoText(state.currency) } - val tokenIconPainter = when (LocalInspectionMode.current) { - // show drawable res in preview - true -> painterResource(id = R.drawable.img_stellar_22) - false -> rememberAsyncImagePainter(model = state.iconUrl) + val (alpha, colorFilter) = remember(state.iconState.isGrayscale) { + if (state.iconState.isGrayscale) { + GRAY_SCALE_ALPHA to GrayscaleColorFilter + } else { + NORMAL_ALPHA to null + } } - - Image( + CurrencyIcon( modifier = Modifier.size(TangemTheme.dimens.size48), - painter = tokenIconPainter, - contentDescription = null, + icon = state.iconState, + alpha = alpha, + colorFilter = colorFilter, ) } } @@ -110,6 +115,9 @@ private fun extractNetwork(tokenCurrency: TokenInfoBlockState.Currency.Token): E private data class ExtractedTokenNetworkText(val normalText: String, val boldText: String) +private val GrayscaleColorFilter: ColorFilter + get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }) + @Preview @Composable private fun Preview_TokenInfoBlock_LightTheme( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index a8f37d0bf3..2b0a8ff76e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -30,7 +30,6 @@ import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRout import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenScreenEvent import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory -import com.tangem.features.tokendetails.navigation.TokenDetailsArguments import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -59,15 +58,14 @@ internal class TokenDetailsViewModel @Inject constructor( private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, private val listenToFlipsUseCase: ListenToFlipsUseCase, private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, - private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { - private val screenArgument: TokenDetailsArguments = savedStateHandle[TokenDetailsRouter.TOKEN_DETAILS_ARGS] - ?: error("This screen can't open without TokenDetailsArgument") + private val cryptoCurrency: CryptoCurrency = savedStateHandle[TokenDetailsRouter.CRYPTO_CURRENCY_KEY] + ?: error("This screen can't open without CryptoCurrency") var router by Delegates.notNull() @@ -75,7 +73,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val refreshStateJobHolder = JobHolder() private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var wallet by Delegates.notNull() - private var cryptoCurrency by Delegates.notNull() private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() @@ -83,34 +80,25 @@ internal class TokenDetailsViewModel @Inject constructor( currentStateProvider = Provider { uiState }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), clickIntents = this, - currencySymbolProvider = Provider { cryptoCurrency.symbol }, - currencyDecimalsProvider = Provider { cryptoCurrency.decimals }, + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, ) - var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(screenArgument)) + var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(cryptoCurrency)) private set override fun onCreate(owner: LifecycleOwner) { - initRequiredFields() + getWallet() + updateContent(selectedWallet = wallet) handleBalanceHiding(owner) } - private fun initRequiredFields() { + private fun getWallet() { getSelectedWalletUseCase() .fold( ifLeft = { error("Can not get selected wallet $it") }, ifRight = { wallet = it }, ) - viewModelScope.launch { - getCryptoCurrencyUseCase(userWalletId = wallet.walletId, id = screenArgument.currencyId) - .fold( - ifLeft = { error("Can not get cryptoCurrency with given ID: screenArgument.currencyId. $it") }, - ifRight = { - cryptoCurrency = it - updateContent(selectedWallet = wallet) - }, - ) - } } private fun updateContent(selectedWallet: UserWallet) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 2c80ed2af1..2c7205d680 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -19,7 +19,6 @@ import androidx.navigation.navArgument import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController -import com.tangem.core.ui.extensions.networkIconResId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.WalletFragment @@ -27,7 +26,6 @@ import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScree import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel -import com.tangem.features.tokendetails.navigation.TokenDetailsArguments import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import kotlin.properties.Delegates @@ -113,23 +111,7 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr reduxNavController.navigate( action = NavigationAction.NavigateTo( screen = AppScreen.WalletDetails, - bundle = bundleOf( - TokenDetailsRouter.TOKEN_DETAILS_ARGS to TokenDetailsArguments( - currencyId = currency.id, - currencyName = currency.name, - currencySymbol = currency.symbol, - iconUrl = currency.iconUrl, - coinType = when (currency) { - is CryptoCurrency.Coin -> TokenDetailsArguments.CoinType.Native - is CryptoCurrency.Token -> TokenDetailsArguments.CoinType.Token( - isCustom = currency.isCustom, - standardName = currency.network.standardType.name, - networkName = currency.network.name, - networkIcon = currency.networkIconResId, - ) - }, - ), - ), + bundle = bundleOf(TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency), ), ) }