Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-31 12:21:36 +02:00
parent 590888db59
commit 682349e353
48 changed files with 860 additions and 111 deletions

View file

@ -12,8 +12,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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
@ -29,6 +27,8 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA
import com.tangem.core.ui.utils.GrayscaleColorFilter
import com.tangem.feature.swap.models.states.PercentDifference
import com.tangem.feature.swap.models.states.ProviderState
@ -38,11 +38,6 @@ import com.tangem.feature.swap.models.states.ProviderState
* https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=7856-41909&mode=design&t=vo7dyElitnzSPSW3-4
*/
private const val GRAY_SCALE_SATURATION = 0f
private const val GRAY_SCALE_ALPHA = 0.4f
private val GrayscaleColorFilter: ColorFilter
get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) })
@Composable
fun ProviderItemBlock(state: ProviderState, modifier: Modifier = Modifier) {
if (state !is ProviderState.Empty) {

View file

@ -1,5 +1,6 @@
package com.tangem.features.tokendetails.featuretoggles
interface TokenDetailsFeatureToggles {
fun isGenerateXPubEnabled(): Boolean
}

View file

@ -75,6 +75,7 @@ dependencies {
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.transaction)
implementation(projects.domain.staking)
/** Temp dependency to swap domain */
implementation(projects.features.swap.domain)

View file

@ -6,5 +6,6 @@ import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggle
internal class DefaultTokenDetailsFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : TokenDetailsFeatureToggles {
override fun isGenerateXPubEnabled() = featureTogglesManager.isFeatureEnabled(name = "GENERATE_XPUB_ENABLED")
}

View file

@ -35,7 +35,7 @@ internal object TokenDetailsPreviewData {
val tokenInfoBlockStateWithLongNameInMainCurrency = TokenInfoBlockState(
name = "Stellar (XLM) with long name test",
iconState = TokenInfoBlockState.IconState.CoinIcon(
iconState = IconState.CoinIcon(
url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
fallbackResId = R.drawable.img_stellar_22,
isGrayscale = false,
@ -44,7 +44,7 @@ internal object TokenDetailsPreviewData {
)
val tokenInfoBlockStateWithLongName = TokenInfoBlockState(
name = "Tether (USDT) with long name test",
iconState = TokenInfoBlockState.IconState.TokenIcon(
iconState = IconState.TokenIcon(
url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
fallbackTint = Color.Cyan,
fallbackBackground = Color.Blue,
@ -59,7 +59,7 @@ internal object TokenDetailsPreviewData {
val tokenInfoBlockStateWithLongNameNoStandard = TokenInfoBlockState(
name = "Tether (USDT) with long name test",
iconState = TokenInfoBlockState.IconState.TokenIcon(
iconState = IconState.TokenIcon(
url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
fallbackTint = Color.Cyan,
fallbackBackground = Color.Blue,
@ -74,7 +74,7 @@ internal object TokenDetailsPreviewData {
val tokenInfoBlockState = TokenInfoBlockState(
name = "Tether USDT",
iconState = TokenInfoBlockState.IconState.CustomTokenIcon(
iconState = IconState.CustomTokenIcon(
tint = Color.Green,
background = Color.Magenta,
isGrayscale = true,
@ -86,6 +86,13 @@ internal object TokenDetailsPreviewData {
),
)
val iconState = IconState.TokenIcon(
url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
fallbackTint = Color.Cyan,
fallbackBackground = Color.Blue,
isGrayscale = false,
)
private val actionButtons = persistentListOf(
TokenDetailsActionButton.Buy(dimContent = false, onClick = {}),
TokenDetailsActionButton.Send(dimContent = false, onClick = {}),
@ -103,6 +110,8 @@ internal object TokenDetailsPreviewData {
private val marketPriceLoading = MarketPriceBlockState.Loading(currencySymbol = "USDT")
private val stakingLoading = StakingBlockState.Loading(iconState = iconState)
private val pullToRefreshConfig = TokenDetailsPullToRefreshConfig(
isRefreshing = false,
onRefresh = {},
@ -237,6 +246,7 @@ internal object TokenDetailsPreviewData {
tokenInfoBlockState = tokenInfoBlockState,
tokenBalanceBlockState = balanceLoading,
marketPriceBlockState = marketPriceLoading,
stakingBlockState = stakingLoading,
notifications = persistentListOf(),
txHistoryState = TxHistoryState.Content(
contentItems = MutableStateFlow(
@ -250,6 +260,7 @@ internal object TokenDetailsPreviewData {
bottomSheetConfig = null,
isBalanceHidden = false,
isMarketPriceAvailable = false,
isStakingAvailable = false,
event = consumedEvent(),
)
@ -267,6 +278,12 @@ internal object TokenDetailsPreviewData {
type = PriceChangeType.UP,
),
),
stakingBlockState = StakingBlockState.Content(
interestRate = "7.38",
periodInDays = 4,
tokenSymbol = "XLM",
iconState = iconState,
),
notifications = persistentListOf(),
txHistoryState = TxHistoryState.NotSupported(
onExploreClick = {},
@ -279,6 +296,7 @@ internal object TokenDetailsPreviewData {
bottomSheetConfig = null,
isBalanceHidden = false,
isMarketPriceAvailable = true,
isStakingAvailable = true,
event = consumedEvent(),
)

View file

@ -0,0 +1,30 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
@Immutable
internal 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()
}

View file

@ -0,0 +1,20 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.compose.runtime.Immutable
@Immutable
internal sealed interface StakingBlockState {
val iconState: IconState
data class Error(override val iconState: IconState) : StakingBlockState
data class Loading(override val iconState: IconState) : StakingBlockState
data class Content(
override val iconState: IconState,
val interestRate: String,
val periodInDays: Int,
val tokenSymbol: String,
) : StakingBlockState
}

View file

@ -17,6 +17,7 @@ internal data class TokenDetailsState(
val tokenInfoBlockState: TokenInfoBlockState,
val tokenBalanceBlockState: TokenDetailsBalanceBlockState,
val marketPriceBlockState: MarketPriceBlockState,
val stakingBlockState: StakingBlockState,
val notifications: ImmutableList<TokenDetailsNotification>,
val pendingTxs: PersistentList<TransactionState>,
val swapTxs: PersistentList<SwapTransactionsState>,
@ -26,5 +27,6 @@ internal data class TokenDetailsState(
val bottomSheetConfig: TangemBottomSheetConfig?,
val isBalanceHidden: Boolean,
val isMarketPriceAvailable: Boolean,
val isStakingAvailable: Boolean,
val event: StateEvent<TextReference>,
)

View file

@ -2,7 +2,6 @@ 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,
@ -11,7 +10,7 @@ internal data class TokenInfoBlockState(
) {
@Immutable
sealed class Currency {
object Native : Currency()
data object Native : Currency()
/**
* @param standardName - token standard. Samples: ERC20, BEP20, BEP2, TRC20 and etc.
@ -24,29 +23,4 @@ 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()
}
}

View file

@ -4,39 +4,39 @@ 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.feature.tokendetails.presentation.tokendetails.state.IconState
import com.tangem.utils.converter.Converter
internal class TokenDetailsIconStateConverter : Converter<CryptoCurrency, TokenInfoBlockState.IconState> {
internal class TokenDetailsIconStateConverter : Converter<CryptoCurrency, IconState> {
override fun convert(value: CryptoCurrency): TokenInfoBlockState.IconState {
override fun convert(value: CryptoCurrency): 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(
private fun getIconStateForCoin(coin: CryptoCurrency.Coin): IconState.CoinIcon {
return IconState.CoinIcon(
url = coin.iconUrl,
fallbackResId = coin.networkIconResId,
isGrayscale = coin.network.isTestnet,
)
}
private fun getIconStateForToken(token: CryptoCurrency.Token): TokenInfoBlockState.IconState {
private fun getIconStateForToken(token: CryptoCurrency.Token): IconState {
val isGrayscale = token.network.isTestnet
val background = token.tryGetBackgroundForTokenIcon(isGrayscale)
val tint = getTintForTokenIcon(background)
return if (token.isCustom && token.iconUrl == null) {
TokenInfoBlockState.IconState.CustomTokenIcon(
IconState.CustomTokenIcon(
tint = tint,
background = background,
isGrayscale = isGrayscale,
)
} else {
TokenInfoBlockState.IconState.TokenIcon(
IconState.TokenIcon(
url = token.iconUrl,
isGrayscale = isGrayscale,
fallbackTint = tint,

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
@ -16,6 +17,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.Toke
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
import com.tangem.features.tokendetails.impl.R
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -25,11 +27,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
internal class TokenDetailsSkeletonStateConverter(
private val clickIntents: TokenDetailsClickIntents,
private val featureToggles: TokenDetailsFeatureToggles,
private val stakingAvailabilityProvider: Provider<StakingAvailability>,
) : Converter<CryptoCurrency, TokenDetailsState> {
private val iconStateConverter by lazy { TokenDetailsIconStateConverter() }
override fun convert(value: CryptoCurrency): TokenDetailsState {
val iconState = iconStateConverter.convert(value)
return TokenDetailsState(
topAppBarConfig = TokenDetailsTopAppBarConfig(
onBackClick = clickIntents::onBackClick,
@ -37,7 +41,7 @@ internal class TokenDetailsSkeletonStateConverter(
),
tokenInfoBlockState = TokenInfoBlockState(
name = value.name,
iconState = iconStateConverter.convert(value),
iconState = iconState,
currency = when (value) {
is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native
is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token(
@ -49,6 +53,7 @@ internal class TokenDetailsSkeletonStateConverter(
),
tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading(actionButtons = createButtons()),
marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol),
stakingBlockState = StakingBlockState.Loading(iconState = iconState),
notifications = persistentListOf(),
pendingTxs = persistentListOf(),
swapTxs = persistentListOf(),
@ -62,6 +67,7 @@ internal class TokenDetailsSkeletonStateConverter(
bottomSheetConfig = null,
isBalanceHidden = true,
isMarketPriceAvailable = value.id.rawCurrencyId != null,
isStakingAvailable = stakingAvailabilityProvider.invoke() is StakingAvailability.Available,
event = consumedEvent(),
)
}

View file

@ -16,6 +16,8 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -38,10 +40,11 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@Suppress("TooManyFunctions", "LargeClass")
@Suppress("TooManyFunctions", "LargeClass", "LongParameterList")
internal class TokenDetailsStateFactory(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val stakingAvailabilityProvider: Provider<StakingAvailability>,
private val clickIntents: TokenDetailsClickIntents,
private val featureToggles: TokenDetailsFeatureToggles,
symbol: String,
@ -52,6 +55,7 @@ internal class TokenDetailsStateFactory(
TokenDetailsSkeletonStateConverter(
clickIntents = clickIntents,
featureToggles = featureToggles,
stakingAvailabilityProvider = stakingAvailabilityProvider,
)
}
@ -98,6 +102,12 @@ internal class TokenDetailsStateFactory(
)
}
private val stakingStateConverter by lazy {
TokenStakingStateConverter(
currentStateProvider = currentStateProvider,
)
}
fun getInitialState(screenArgument: CryptoCurrency): TokenDetailsState {
return skeletonStateConverter.convert(value = screenArgument)
}
@ -201,6 +211,12 @@ internal class TokenDetailsStateFactory(
)
}
fun getStateWithStaking(stakingEither: Either<Throwable, StakingEntryInfo>): TokenDetailsState {
return currentStateProvider().copy(
stakingBlockState = stakingStateConverter.convert(stakingEither),
)
}
fun getRefreshingState(): TokenDetailsState {
return refreshStateConverter.convert(true)
}

View file

@ -0,0 +1,35 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import arrow.core.Either
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
internal class TokenStakingStateConverter(
private val currentStateProvider: Provider<TokenDetailsState>,
) : Converter<Either<Throwable, StakingEntryInfo>, StakingBlockState> {
override fun convert(value: Either<Throwable, StakingEntryInfo>): StakingBlockState {
value.fold(
ifLeft = {
return StakingBlockState.Error(
iconState = currentStateProvider().tokenInfoBlockState.iconState,
)
},
ifRight = {
return StakingBlockState.Content(
interestRate = BigDecimalFormatter.formatPercent(
percent = it.interestRate,
useAbsoluteValue = true,
),
periodInDays = it.periodInDays,
tokenSymbol = it.tokenSymbol,
iconState = currentStateProvider().tokenInfoBlockState.iconState,
)
},
)
}
}

View file

@ -42,8 +42,10 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.*
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
@ -133,6 +135,14 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
)
}
if (state.isStakingAvailable) {
item(
key = StakingBlockState::class.java,
contentType = StakingBlockState::class.java,
content = { TokenStakingBlock(modifier = itemModifier, state = state.stakingBlockState) },
)
}
swapTransactionsItems(
state.swapTxs,
itemModifier,

View file

@ -21,26 +21,21 @@ import coil.request.ImageRequest
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState
import com.tangem.features.tokendetails.impl.R
import kotlinx.coroutines.launch
@Composable
internal fun CurrencyIcon(
icon: TokenInfoBlockState.IconState,
alpha: Float,
colorFilter: ColorFilter?,
modifier: Modifier = Modifier,
) {
internal fun CurrencyIcon(icon: IconState, alpha: Float, colorFilter: ColorFilter?, modifier: Modifier = Modifier) {
when (icon) {
is TokenInfoBlockState.IconState.CoinIcon -> CoinIcon(
is IconState.CoinIcon -> CoinIcon(
modifier = modifier,
url = icon.url,
fallbackResId = icon.fallbackResId,
alpha = alpha,
colorFilter = colorFilter,
)
is TokenInfoBlockState.IconState.TokenIcon -> TokenIcon(
is IconState.TokenIcon -> TokenIcon(
modifier = modifier,
url = icon.url,
alpha = alpha,
@ -54,7 +49,7 @@ internal fun CurrencyIcon(
)
},
)
is TokenInfoBlockState.IconState.CustomTokenIcon -> CustomTokenIcon(
is IconState.CustomTokenIcon -> CustomTokenIcon(
modifier = modifier,
tint = icon.tint,
background = icon.background,

View file

@ -11,8 +11,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
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.tooling.preview.Preview
@ -20,14 +18,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA
import com.tangem.core.ui.utils.GrayscaleColorFilter
import com.tangem.core.ui.utils.NORMAL_ALPHA
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(
@ -132,10 +129,10 @@ 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) })
private data class ExtractedTokenNetworkText(
val normalText: String,
val boldText: String,
)
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -0,0 +1,206 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA
import com.tangem.core.ui.utils.GrayscaleColorFilter
import com.tangem.core.ui.utils.NORMAL_ALPHA
import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState
import com.tangem.features.tokendetails.impl.R
/**
* Token staking block
*
* @param state component state
* @param modifier modifier
*/
@Composable
internal fun TokenStakingBlock(state: StakingBlockState, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size72)
.padding(all = TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
horizontalAlignment = Alignment.Start,
) {
Content(state = state)
}
}
@Composable
private fun Content(state: StakingBlockState, modifier: Modifier = Modifier) {
AnimatedContent(
modifier = modifier.heightIn(min = TangemTheme.dimens.size60),
targetState = state,
contentAlignment = Alignment.CenterStart,
label = "Update the content",
) { stakingBlockState ->
when (stakingBlockState) {
is StakingBlockState.Content -> {
StakingContent(
stakingBlockState = stakingBlockState,
iconState = stakingBlockState.iconState,
)
}
is StakingBlockState.Loading -> {
StakingLoading(
iconState = stakingBlockState.iconState,
)
}
is StakingBlockState.Error -> Row {} // TODO staking
}
}
}
@Composable
private fun StakingContent(stakingBlockState: StakingBlockState.Content, iconState: IconState) {
Column {
Row {
val (alpha, colorFilter) = remember(iconState.isGrayscale) {
if (iconState.isGrayscale) {
GRAY_SCALE_ALPHA to GrayscaleColorFilter
} else {
NORMAL_ALPHA to null
}
}
CurrencyIcon(
modifier = Modifier
.size(TangemTheme.dimens.size20)
.clip(TangemTheme.shapes.roundedCorners8)
.align(Alignment.CenterVertically),
icon = iconState,
alpha = alpha,
colorFilter = colorFilter,
)
SpacerW8()
Column {
Text(
text = stringResource(
R.string.token_details_staking_block_title,
stakingBlockState.interestRate,
),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size4))
Text(
text = stringResource(
R.string.token_details_staking_block_subtitle,
stakingBlockState.tokenSymbol,
stakingBlockState.periodInDays,
),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size8))
}
}
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = "Stake",
onClick = { /* [REDACTED_TODO_COMMENT] */ },
)
}
}
@Composable
private fun StakingLoading(iconState: IconState) {
Column {
Row {
val (alpha, colorFilter) = remember(iconState.isGrayscale) {
if (iconState.isGrayscale) {
GRAY_SCALE_ALPHA to GrayscaleColorFilter
} else {
NORMAL_ALPHA to null
}
}
CurrencyIcon(
modifier = Modifier
.size(TangemTheme.dimens.size20)
.clip(TangemTheme.shapes.roundedCorners8)
.align(Alignment.CenterVertically),
icon = iconState,
alpha = alpha,
colorFilter = colorFilter,
)
SpacerW8()
Column {
RectangleShimmer(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size20),
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size4))
RectangleShimmer(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size20),
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size8))
}
}
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = "Loading", // TODO staking
onClick = { /* [REDACTED_TODO_COMMENT] */ },
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_TokenStakingBlock(
@PreviewParameter(StakingBlockStateProvider::class)
state: StakingBlockState,
) {
TangemThemePreview {
TokenStakingBlock(state = state)
}
}
private class StakingBlockStateProvider : CollectionPreviewParameterProvider<StakingBlockState>(
collection = listOf(
StakingBlockState.Content(
iconState = iconState,
interestRate = "10",
periodInDays = 4,
tokenSymbol = "SOL",
),
StakingBlockState.Loading(iconState = iconState),
StakingBlockState.Error(iconState = iconState),
),
)
private val iconState = IconState.TokenIcon(
url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
fallbackTint = Color.Cyan,
fallbackBackground = Color.Blue,
isGrayscale = false,
)
// endregion Preview

View file

@ -27,6 +27,9 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo
@ -97,6 +100,8 @@ internal class TokenDetailsViewModel @Inject constructor(
private val shouldShowSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase,
private val getStakingEntryInfoUseCase: GetStakingEntryInfoUseCase,
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
private val swapRepository: SwapRepository,
private val swapTransactionRepository: SwapTransactionRepository,
private val quotesRepository: QuotesRepository,
@ -107,8 +112,8 @@ internal class TokenDetailsViewModel @Inject constructor(
private val analyticsEventsHandler: AnalyticsEventHandler,
private val hapticManager: HapticManager,
private val clipboardManager: ClipboardManager,
tokenDetailsFeatureToggles: TokenDetailsFeatureToggles,
getUserWalletUseCase: GetUserWalletUseCase,
featureToggles: TokenDetailsFeatureToggles,
deepLinksRegistry: DeepLinksRegistry,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents {
@ -137,10 +142,13 @@ internal class TokenDetailsViewModel @Inject constructor(
private val stateFactory = TokenDetailsStateFactory(
currentStateProvider = Provider { uiState },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
stakingAvailabilityProvider = Provider {
getStakingAvailabilityUseCase.invoke(cryptoCurrency.network.id.value)
},
clickIntents = this,
symbol = cryptoCurrency.symbol,
decimals = cryptoCurrency.decimals,
featureToggles = featureToggles,
featureToggles = tokenDetailsFeatureToggles,
)
private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
@ -207,6 +215,7 @@ internal class TokenDetailsViewModel @Inject constructor(
subscribeOnCurrencyStatusUpdates()
subscribeOnExchangeTransactionsUpdates()
updateTxHistory(refresh = false, showItemsLoading = true)
updateStakingInfo()
}
private fun handleBalanceHiding(owner: LifecycleOwner) {
@ -358,6 +367,16 @@ internal class TokenDetailsViewModel @Inject constructor(
}
}
private fun updateStakingInfo() {
viewModelScope.launch(dispatchers.io) {
val stakingAvailability = getStakingAvailabilityUseCase(cryptoCurrency.network.id.value)
if (stakingAvailability is StakingAvailability.Available) {
val stakingInfo = getStakingEntryInfoUseCase(stakingAvailability.integrationId)
uiState = stateFactory.getStateWithStaking(stakingInfo)
}
}
}
private fun updateTopBarMenu() {
viewModelScope.launch(dispatchers.main) {
uiState = stateFactory.getStateWithUpdatedMenu(