Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-19 10:20:09 +03:00
commit f0e4aec9a4
858 changed files with 13724 additions and 5705 deletions

View file

@ -271,6 +271,7 @@ private fun AmountFieldError(
style = TangemTheme.typography.caption2,
color = color,
textAlign = TextAlign.Center,
modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_ERROR_TEXT),
)
}
}

View file

@ -1,76 +0,0 @@
package com.tangem.common.ui.news
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
internal fun ArticleBadge(articleTagUM: ArticleTagUM, modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier
.heightIn(min = 24.dp)
.background(
color = TangemTheme.colors.icon.informative.copy(alpha = 0.1f),
shape = RoundedCornerShape(8.dp),
)
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
when (articleTagUM) {
is ArticleTagUM.Category -> Unit
is ArticleTagUM.Token -> {
CurrencyIcon(
state = articleTagUM.iconState,
shouldDisplayNetwork = false,
iconSize = 16.dp,
)
}
}
Text(
text = articleTagUM.title.resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.secondary,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ArticleBadgePreview() {
TangemThemePreview {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
ArticleBadge(
articleTagUM = ArticleTagUM.Token(
TextReference.Str("BTC"),
iconState = CurrencyIconState.CoinIcon(
url = "",
fallbackResId = 0,
isGrayscale = false,
shouldShowCustomBadge = false,
),
),
)
ArticleBadge(
articleTagUM = ArticleTagUM.Category(TextReference.Str("Regulation")),
)
}
}
}

View file

@ -1,41 +1,48 @@
package com.tangem.common.ui.news
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CardColors
import androidx.compose.material3.Text
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.drawWithCache
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.collections.immutable.toPersistentList
@Composable
fun ArticleCard(articleConfigUM: ArticleConfigUM, onArticleClick: () -> Unit, modifier: Modifier = Modifier) {
fun ArticleCard(
articleConfigUM: ArticleConfigUM,
onArticleClick: () -> Unit,
modifier: Modifier = Modifier,
colors: CardColors = TangemBlockCardColors,
) {
BlockCard(
modifier = modifier,
onClick = onArticleClick,
colors = colors,
) {
if (articleConfigUM.isTrending) {
TrendingArticle(articleConfigUM = articleConfigUM)
@ -81,7 +88,7 @@ private fun TrendingArticle(articleConfigUM: ArticleConfigUM) {
ArticleInfo(
score = articleConfigUM.score,
createdAt = articleConfigUM.createdAt,
createdAt = articleConfigUM.createdAt.resolveReference(),
)
SpacerH(32.dp)
@ -98,7 +105,7 @@ private fun DefaultArticle(articleConfigUM: ArticleConfigUM) {
Column(modifier = Modifier.padding(12.dp)) {
ArticleInfo(
score = articleConfigUM.score,
createdAt = articleConfigUM.createdAt,
createdAt = articleConfigUM.createdAt.resolveReference(),
)
SpacerH(8.dp)
@ -121,54 +128,13 @@ private fun DefaultArticle(articleConfigUM: ArticleConfigUM) {
}
}
@Composable
private fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) {
val dotColor = TangemTheme.colors.text.secondary
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12),
contentDescription = null,
)
Text(
text = score.toString(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
Spacer(
modifier = Modifier
.size(4.dp)
.drawWithCache {
val radius = size.minDimension / 2f
onDrawBehind {
drawCircle(
color = dotColor,
radius = radius,
)
}
},
)
Text(
text = createdAt,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun Tags(tags: ImmutableList<ArticleTagUM>, modifier: Modifier = Modifier) {
private fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
val expandIndicator = remember {
ContextualFlowRowOverflow.expandIndicator {
val remainingItems = tags.size - shownItemCount
ArticleBadge(articleTagUM = ArticleTagUM.Category(TextReference.Str("${StringsSigns.PLUS}$remainingItems")))
Label(state = LabelUM(TextReference.Str("${StringsSigns.PLUS}$remainingItems")))
}
}
ContextualFlowRow(
@ -179,7 +145,7 @@ private fun Tags(tags: ImmutableList<ArticleTagUM>, modifier: Modifier = Modifie
maxLines = 1,
overflow = expandIndicator,
) { index ->
ArticleBadge(articleTagUM = tags[index])
Label(state = tags[index])
}
}
@ -189,14 +155,14 @@ private fun Tags(tags: ImmutableList<ArticleTagUM>, modifier: Modifier = Modifie
private fun TagsPreview() {
TangemThemePreview {
Tags(
tags = listOf(
ArticleTagUM.Category(TextReference.Str("Hype")),
ArticleTagUM.Category(TextReference.Str("BTC")),
ArticleTagUM.Category(TextReference.Str("Supply")),
ArticleTagUM.Category(TextReference.Str("Demand")),
ArticleTagUM.Category(TextReference.Str("Best rate")),
ArticleTagUM.Category(TextReference.Str("Breaking news")),
).toPersistentList(),
tags = persistentListOf(
LabelUM(TextReference.Str("Hype")),
LabelUM(TextReference.Str("BTC")),
LabelUM(TextReference.Str("Supply")),
LabelUM(TextReference.Str("Demand")),
LabelUM(TextReference.Str("Best rate")),
LabelUM(TextReference.Str("Breaking news")),
),
)
}
}
@ -206,19 +172,18 @@ private fun TagsPreview() {
@Composable
private fun ArticleCardsPreview() {
val tags = listOf(
ArticleTagUM.Category(TextReference.Str("Hype")),
ArticleTagUM.Category(TextReference.Str("BTC")),
ArticleTagUM.Category(TextReference.Str("Supply")),
ArticleTagUM.Category(TextReference.Str("Demand")),
ArticleTagUM.Category(TextReference.Str("Best rate")),
ArticleTagUM.Category(TextReference.Str("Breaking news")),
LabelUM(TextReference.Str("Hype")),
LabelUM(TextReference.Str("BTC")),
LabelUM(TextReference.Str("Supply")),
LabelUM(TextReference.Str("Demand")),
LabelUM(TextReference.Str("Breaking news")),
).toImmutableSet()
val config = ArticleConfigUM(
id = 1,
title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)",
score = 9.5f,
createdAt = "1h ago",
createdAt = TextReference.Str("1h ago"),
isTrending = true,
tags = tags,
isViewed = false,

View file

@ -1,13 +1,15 @@
package com.tangem.common.ui.news
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableSet
data class ArticleConfigUM(
val id: Int,
val title: String,
val score: Float,
val createdAt: String,
val createdAt: TextReference,
val isTrending: Boolean,
val tags: ImmutableSet<ArticleTagUM>,
val tags: ImmutableSet<LabelUM>,
val isViewed: Boolean,
)

View file

@ -0,0 +1,55 @@
package com.tangem.common.ui.news
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.res.TangemTheme
import kotlinx.collections.immutable.ImmutableList
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ArticleHeader(
title: String,
createdAt: String,
score: Float,
tags: ImmutableList<LabelUM>,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
ArticleInfo(
score = score,
createdAt = createdAt,
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = title,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
if (tags.isNotEmpty()) {
Spacer(modifier = Modifier.height(20.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
tags.forEach { tag ->
Label(
state = tag,
)
}
}
}
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.common.ui.news
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) {
val dotColor = TangemTheme.colors.text.secondary
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12),
contentDescription = null,
)
Text(
text = score.toString(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
Spacer(
modifier = Modifier
.size(4.dp)
.drawWithCache {
val radius = size.minDimension / 2f
onDrawBehind {
drawCircle(
color = dotColor,
radius = radius,
)
}
},
)
Text(
text = createdAt,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
}
}

View file

@ -0,0 +1,63 @@
package com.tangem.common.ui.news
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.res.TangemTheme
@Composable
fun TrendingLoadingArticle(modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp, horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
RectangleShimmer(modifier = Modifier.size(width = 96.dp, height = 24.dp), radius = 8.dp)
SpacerH(12.dp)
RectangleShimmer(modifier = Modifier.size(width = 285.dp, height = 18.dp), radius = 4.dp)
SpacerH(6.dp)
RectangleShimmer(modifier = Modifier.size(width = 190.dp, height = 18.dp), radius = 4.dp)
SpacerH(14.dp)
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 18.dp), radius = 4.dp)
SpacerH(32.dp)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
}
}
}
}
@Composable
fun DefaultLoadingArticle() {
BlockCard(
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
Column(modifier = Modifier.padding(12.dp)) {
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 16.dp), radius = 4.dp)
SpacerH(12.dp)
RectangleShimmer(modifier = Modifier.size(width = 142.dp, height = 18.dp), radius = 4.dp)
SpacerH(6.dp)
RectangleShimmer(modifier = Modifier.size(width = 176.dp, height = 18.dp), radius = 4.dp)
SpacerH(6.dp)
RectangleShimmer(modifier = Modifier.size(width = 120.dp, height = 18.dp), radius = 4.dp)
SpacerH(16.dp)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
}
}
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.common.ui.news
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
@Immutable
sealed interface ArticleTagUM {
val title: TextReference
data class Category(override val title: TextReference) : ArticleTagUM
data class Token(
override val title: TextReference,
val iconState: CurrencyIconState,
) : ArticleTagUM
}

View file

@ -20,7 +20,7 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.yieldSupplyKey
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingOption
import com.tangem.domain.staking.model.stakekit.Yield
@ -171,7 +171,7 @@ class TokenItemStateConverter(
return totalAmount.format { crypto(currency) }
}
private fun CryptoCurrencyStatus.getStakedBalance() = (value.yieldBalance as? YieldBalance.Data)
private fun CryptoCurrencyStatus.getStakedBalance() = (value.stakingBalance as? StakingBalance.Data)
?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.rawId).orZero()
private fun createTitleState(
@ -277,14 +277,18 @@ class TokenItemStateConverter(
val stakingAvailability = stakingApyMap[currencyStatus.currency] as? StakingAvailability.Available
?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
val yieldBalance = currencyStatus.value.yieldBalance
val hasStakedBalance = yieldBalance is YieldBalance.Data
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
val rateInfo = when (val stakingOptions = stakingAvailability.option) {
is StakingOption.P2P -> null // todo p2p
is StakingOption.StakeKit -> if (hasStakedBalance) {
is StakingOption.P2P -> {
// P2P or no balance: use preferred validators
// TODO add p2p logic
null
}
is StakingOption.StakeKit -> if (stakeKitBalance != null) {
val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address }
yieldBalance.balance.items
stakeKitBalance.balance.items
.mapNotNull { it.validatorAddress }
.firstNotNullOfOrNull { address ->
validatorsByAddress[address]?.rewardInfo
@ -306,7 +310,7 @@ class TokenItemStateConverter(
return StakingLocalInfo(
rate = rateInfo?.rate,
isActive = hasStakedBalance,
isActive = stakeKitBalance != null, // todo add p2p check
rewardType = rateInfo?.type,
)
}

View file

@ -1,6 +1,8 @@
package com.tangem.common.ui.userwallet
import com.tangem.common.ui.R
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.SignIn
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -13,6 +15,8 @@ import com.tangem.domain.common.wallets.error.UnlockWalletError.UnableToUnlock.R
inline fun UnlockWalletError.handle(
onAlreadyUnlocked: () -> Unit = {},
onUserCancelled: () -> Unit = {},
analyticsEventHandler: AnalyticsEventHandler,
isFromUnlockAll: Boolean,
noinline showMessage: (EventMessage) -> Unit,
) {
when (this) {
@ -30,15 +34,26 @@ inline fun UnlockWalletError.handle(
// This should never happen in this flow, as we always check for the wallet existence before unlocking
showMessage(SnackbarMessage(TextReference.Res(R.string.generic_error)))
}
is UnlockWalletError.UnableToUnlock -> handleUnableToUnlock(this, showMessage)
is UnlockWalletError.UnableToUnlock -> handleUnableToUnlock(
isFromUnlockAll = isFromUnlockAll,
error = this,
analyticsEventHandler = analyticsEventHandler,
showDialog = showMessage,
)
}
}
fun handleUnableToUnlock(error: UnlockWalletError.UnableToUnlock, showDialog: (DialogMessage) -> Unit) {
fun handleUnableToUnlock(
isFromUnlockAll: Boolean,
error: UnlockWalletError.UnableToUnlock,
analyticsEventHandler: AnalyticsEventHandler,
showDialog: (DialogMessage) -> Unit,
) {
val dialogMessage = when (error) {
is UnlockWalletError.UnableToUnlock.WithReason -> {
when (error.reason) {
Reason.AllKeysInvalidated -> {
analyticsEventHandler.send(SignIn.ErrorBiometricUpdated(isFromUnlockAll))
DialogMessage(
title = resourceReference(R.string.biometric_updated_warning_title),
message = resourceReference(R.string.biometric_updated_warning_description),