Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-05 05:47:09 +02:00
parent 471343e66b
commit d9a539b64d
11 changed files with 287 additions and 62 deletions

View file

@ -9,6 +9,7 @@ import androidx.compose.runtime.Immutable
* It necessary to use [Immutable] annotation because all sealed interface has runtime stability.
* All subclasses are stable.
*/
@Immutable
sealed class ImageReference {
data class Url(val url: String) : ImageReference()

View file

@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="744dp"
android:height="744dp"
android:viewportWidth="744"
android:viewportHeight="744">
<path
android:pathData="M0,0h744v744h-744z"
android:fillColor="#136BFF"/>
<path
android:pathData="M110,318C108.89,318 108,318.89 108,320V380C108,381.11 108.89,382 110,382H266C267.11,382 268,381.11 268,380V320C268,318.89 267.11,318 266,318H110ZM247,362C247.55,362 248,361.55 248,361V339C248,338.45 247.55,338 247,338H129C128.45,338 128,338.45 128,339V361C128,361.55 128.45,362 129,362H247Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M128,408C128,406.89 127.11,406 126,406H110C108.89,406 108,406.89 108,408V424C108,425.11 108.89,426 110,426H126C127.11,426 128,425.11 128,424V408Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M496,408C496,406.89 495.11,406 494,406H478C476.89,406 476,406.89 476,408V424C476,425.11 476.89,426 478,426H494C495.11,426 496,425.11 496,424V408Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M292,408C292,406.89 292.89,406 294,406H450C451.11,406 452,406.89 452,408V424C452,425.11 451.11,426 450,426H294C292.89,426 292,425.11 292,424V408Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M476,320C476,318.89 476.89,318 478,318H634C635.1,318 636,318.89 636,320V380C636,381.11 635.1,382 634,382H478C476.89,382 476,381.11 476,380V320ZM615,338C615.55,338 616,338.45 616,339V361C616,361.55 615.55,362 615,362H497C496.45,362 496,361.55 496,361V339C496,338.45 496.45,338 497,338H615Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M294,318C292.89,318 292,318.89 292,320V336C292,337.11 292.89,338 294,338H431C431.55,338 432,338.45 432,339V361C432,361.55 431.55,362 431,362H294C292.89,362 292,362.89 292,364V380C292,381.11 292.89,382 294,382H450C451.11,382 452,381.11 452,380V320C452,318.89 451.11,318 450,318H294Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -25,8 +25,8 @@ sealed interface StakingTarget {
/** Whether this target is active and available for staking */
val isActive: Boolean
/** Image URL for display (validator logo or vault icon) */
val image: String?
/** Image for display (validator logo or vault icon) */
val image: StakingTargetImage?
/** Whether this is a strategic partner (shows special badge in UI) */
val isStrategicPartner: Boolean
@ -40,7 +40,7 @@ sealed interface StakingTarget {
override val rewardInfo: RewardInfo? = delegate.rewardInfo
override val isPreferred: Boolean = delegate.preferred
override val isActive: Boolean = delegate.status == Yield.Validator.ValidatorStatus.ACTIVE
override val image: String? = delegate.image
override val image: StakingTargetImage? = delegate.image?.let { StakingTargetImage.Url(it) }
override val isStrategicPartner: Boolean = delegate.isStrategicPartner
}
@ -56,7 +56,7 @@ sealed interface StakingTarget {
)
override val isPreferred: Boolean = true
override val isActive: Boolean = true
override val image: String? = null
override val image = StakingTargetImage.Local(StakingLocalImageType.P2P_VAULT)
override val isStrategicPartner: Boolean = true
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.staking.model
sealed interface StakingTargetImage {
data class Url(val url: String) : StakingTargetImage
data class Local(val type: StakingLocalImageType) : StakingTargetImage
}
enum class StakingLocalImageType {
P2P_VAULT,
}

View file

@ -7,7 +7,7 @@ package com.tangem.domain.staking.model.ethpool
*/
object P2PEthPoolStakingConfig {
const val USE_TESTNET: Boolean = true
const val USE_TESTNET: Boolean = false
val activeNetwork: P2PEthPoolNetwork
get() = if (USE_TESTNET) P2PEthPoolNetwork.TESTNET else P2PEthPoolNetwork.MAINNET

View file

@ -20,6 +20,8 @@ import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.utils.getRewardTypeShortText
import com.tangem.features.staking.impl.presentation.ui.utils.toDrawableRes
import com.tangem.features.staking.impl.presentation.ui.utils.toImageUrl
import com.tangem.utils.extensions.orZero
@Composable
@ -42,7 +44,8 @@ internal fun StakingClaimRewardsValidatorContent(
caption = item.getAprTextNeutral(),
infoTitle = item.formattedFiatAmount,
infoSubtitle = item.formattedCryptoAmount,
imageUrl = item.target?.image.orEmpty(),
imageUrl = item.target?.image.toImageUrl(),
iconRes = item.target?.image.toDrawableRes(),
onImageError = { ValidatorImagePlaceholder() },
modifier = Modifier
.roundedShapeItemDecoration(index, state.rewards.lastIndex, false)

View file

@ -7,6 +7,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Icon
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
@ -19,7 +20,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle
@ -32,9 +36,9 @@ import androidx.compose.ui.unit.Density
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.components.inputrow.InputRowImageInfo
import com.tangem.core.ui.components.list.roundedListWithDividersItems
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.*
@ -55,6 +59,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.state.utils.getRewardTypeShortText
import com.tangem.features.staking.impl.presentation.ui.utils.toImageReference
import com.tangem.utils.StringsSigns.DOT
import com.tangem.utils.extensions.orZero
@ -275,16 +280,9 @@ private fun ActiveStakingBlock(
modifier: Modifier = Modifier,
) {
val (icon, iconTint) = balance.type.getIcon()
InputRowImageInfo(
subtitle = balance.title,
caption = balance.subtitle ?: balance.getAprTextNeutral(),
infoTitle = balance.formattedFiatAmount.orMaskWithStars(isBalanceHidden),
infoSubtitle = balance.formattedCryptoAmount.orMaskWithStars(isBalanceHidden),
imageUrl = balance.getImage(),
iconRes = icon,
iconTint = iconTint,
subtitleEndIconRes = R.drawable.ic_staking_pending_transaction.takeIf { balance.isPending },
onImageError = { ValidatorImagePlaceholder() },
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.background(TangemTheme.colors.background.action)
.clickable(
@ -295,8 +293,96 @@ private fun ActiveStakingBlock(
onAnalytic()
onClick(balance)
},
),
)
)
.padding(TangemTheme.dimens.spacing12),
) {
StakingBalanceIcon(
balance = balance,
icon = icon,
iconTint = iconTint,
)
Column(modifier = Modifier.weight(1f)) {
Row {
Text(
text = balance.title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
if (balance.isPending) {
Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing4))
Icon(
painter = rememberVectorPainter(
image = ImageVector.vectorResource(id = R.drawable.ic_staking_pending_transaction),
),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens.size18),
)
}
}
val caption = balance.subtitle ?: balance.getAprTextNeutral()
Text(
text = caption.resolveAnnotatedReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
)
}
SpacerW12()
Column(horizontalAlignment = Alignment.End) {
Text(
text = balance.formattedFiatAmount.orMaskWithStars(isBalanceHidden).resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
if (balance.formattedCryptoAmount != null) {
Text(
text = balance.formattedCryptoAmount.orMaskWithStars(isBalanceHidden).resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
)
}
}
}
}
@Composable
private fun RowScope.StakingBalanceIcon(balance: BalanceState, icon: Int?, iconTint: Color) {
if (balance.hasImage() || icon != null) {
StakingTargetIcon(
image = if (balance.hasImage()) balance.target?.image.toImageReference() else null,
onImageError = if (icon != null) {
{
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(TangemTheme.dimens.spacing36)
.clip(TangemTheme.shapes.roundedCornersXLarge)
.background(iconTint.copy(alpha = 0.08f)),
) {
Icon(
painter = rememberVectorPainter(
image = ImageVector.vectorResource(id = icon),
),
tint = iconTint,
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens.size18),
)
}
}
} else {
{ ValidatorImagePlaceholder() }
},
modifier = Modifier
.size(TangemTheme.dimens.spacing36)
.clip(TangemTheme.shapes.roundedCornersXLarge),
)
SpacerW12()
}
}
@Composable
@ -341,13 +427,12 @@ private fun BalanceType.getIcon() = when (this) {
else -> null to TangemTheme.colors.icon.informative
}
@Composable
private fun BalanceState.getImage() = when (type) {
private fun BalanceState.hasImage() = when (type) {
BalanceType.UNSTAKING,
BalanceType.UNSTAKED,
BalanceType.LOCKED,
-> null
else -> target?.image
-> false
else -> target?.image != null
}
private val textGradientColors = listOf(

View file

@ -0,0 +1,35 @@
package com.tangem.features.staking.impl.presentation.ui
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage
import com.tangem.core.ui.extensions.ImageReference
@Composable
internal fun StakingTargetIcon(
image: ImageReference?,
modifier: Modifier = Modifier,
onImageError: (@Composable () -> Unit)? = null,
) {
when (image) {
is ImageReference.Url -> {
InputRowAsyncImage(
imageUrl = image.url,
onImageError = onImageError,
modifier = modifier,
)
}
is ImageReference.Res -> {
Image(
painter = painterResource(image.resId),
contentDescription = null,
modifier = modifier,
)
}
null -> {
onImageError?.invoke()
}
}
}

View file

@ -3,12 +3,16 @@ package com.tangem.features.staking.impl.presentation.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -20,7 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.inputrow.InputRowImageSelector
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.format
@ -35,6 +39,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.previewdata.ValidatorStatePreviewData
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.state.utils.getRewardTypeLongText
import com.tangem.features.staking.impl.presentation.ui.utils.toImageReference
import com.tangem.utils.extensions.orZero
/**
@ -61,47 +66,80 @@ internal fun StakingValidatorListContent(
key = { targets[it].address },
contentType = { targets[it]::class.java },
) { index ->
val item = targets[index]
InputRowImageSelector(
subtitle = stringReference(item.name),
caption = item.getAprTextNeutral(),
imageUrl = item.image.orEmpty(),
isSelected = item == state.chosenTarget,
onSelect = { clickIntents.onTargetSelect(item) },
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = targets.lastIndex,
radius = TangemTheme.dimens.radius12,
addDefaultPadding =
false,
)
.background(TangemTheme.colors.background.action),
subtitleExtraContent = {
ValidatorLabel(item.isStrategicPartner)
},
selectorContent = { checked, _, _ ->
AnimatedVisibility(
modifier = Modifier,
visible = checked,
) {
Icon(
painter = rememberVectorPainter(
image = ImageVector.vectorResource(id = R.drawable.ic_check_24),
),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
}
},
onImageError = { ValidatorImagePlaceholder() },
ValidatorListItem(
item = targets[index],
isSelected = targets[index] == state.chosenTarget,
index = index,
lastIndex = targets.lastIndex,
onSelect = { clickIntents.onTargetSelect(targets[index]) },
)
}
}
}
}
@Composable
private fun ValidatorListItem(
item: StakingTarget,
isSelected: Boolean,
index: Int,
lastIndex: Int,
onSelect: () -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = lastIndex,
radius = TangemTheme.dimens.radius12,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
onClick = onSelect,
)
.padding(TangemTheme.dimens.spacing12),
) {
StakingTargetIcon(
image = item.image.toImageReference(),
onImageError = { ValidatorImagePlaceholder() },
modifier = Modifier
.size(TangemTheme.dimens.spacing36)
.clip(TangemTheme.shapes.roundedCornersXLarge),
)
SpacerW12()
Column(modifier = Modifier.weight(1f)) {
Row {
Text(
text = stringReference(item.name).resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
ValidatorLabel(item.isStrategicPartner)
}
Text(
text = item.getAprTextNeutral().resolveAnnotatedReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
)
}
SpacerW12()
AnimatedVisibility(visible = isSelected) {
Icon(
painter = rememberVectorPainter(
image = ImageVector.vectorResource(id = R.drawable.ic_check_24),
),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
}
}
}
/**
* For FCA fixes remove coloring for now
*/

View file

@ -19,7 +19,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
@ -30,7 +29,9 @@ import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.StakingStates.ValidatorState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.utils.getRewardTypeShortText
import com.tangem.features.staking.impl.presentation.ui.StakingTargetIcon
import com.tangem.features.staking.impl.presentation.ui.ValidatorImagePlaceholder
import com.tangem.features.staking.impl.presentation.ui.utils.toImageReference
import com.tangem.utils.extensions.orZero
@Composable
@ -63,8 +64,8 @@ internal fun ValidatorBlock(validatorState: StakingStates.ValidatorState, isClic
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(12.dp),
) {
InputRowAsyncImage(
imageUrl = state.chosenTarget.image.orEmpty(),
StakingTargetIcon(
image = state.chosenTarget.image.toImageReference(),
onImageError = { ValidatorImagePlaceholder() },
modifier = Modifier
.size(24.dp)

View file

@ -0,0 +1,23 @@
package com.tangem.features.staking.impl.presentation.ui.utils
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.ImageReference
import com.tangem.core.ui.R as CoreR
import com.tangem.domain.staking.model.StakingLocalImageType
import com.tangem.domain.staking.model.StakingTargetImage
fun StakingTargetImage?.toImageReference(): ImageReference? = when (this) {
is StakingTargetImage.Url -> url.takeIf { it.isNotBlank() }?.let { ImageReference.Url(it) }
is StakingTargetImage.Local -> ImageReference.Res(type.toDrawableRes())
null -> null
}
fun StakingTargetImage?.toImageUrl(): String? = (this as? StakingTargetImage.Url)?.url?.takeIf { it.isNotBlank() }
@DrawableRes
fun StakingTargetImage?.toDrawableRes(): Int? = (this as? StakingTargetImage.Local)?.type?.toDrawableRes()
@DrawableRes
fun StakingLocalImageType.toDrawableRes(): Int = when (this) {
StakingLocalImageType.P2P_VAULT -> CoreR.drawable.ic_p2p_logo
}