Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-23 15:14:49 +02:00
parent 9cde8a7ba0
commit 183cb93c1a
13 changed files with 107 additions and 46 deletions

View file

@ -27,13 +27,13 @@ import com.tangem.domain.notifications.SendPushTokenUseCase
import com.tangem.domain.notifications.models.ApplicationId
import com.tangem.domain.notifications.models.NotificationsError
import com.tangem.domain.onramp.FetchHotCryptoUseCase
import com.tangem.domain.stories.GetStoryContentUseCase
import com.tangem.domain.stories.models.StoryContentIds
import com.tangem.domain.quotes.multi.MultiQuoteUpdater
import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingOptionsUseCase
import com.tangem.domain.stories.GetStoryContentUseCase
import com.tangem.domain.stories.models.StoryContentIds
import com.tangem.domain.wallets.usecase.AssociateWalletsWithApplicationIdUseCase
import com.tangem.domain.wallets.usecase.GetSavedWalletsCountUseCase
import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase
@ -268,8 +268,12 @@ internal class MainViewModel @Inject constructor(
onShownBalanceToastAction()
}
},
startIconId = if (settings.isBalanceHidden) {
R.drawable.ic_eye_off_outline_24
} else {
R.drawable.ic_eye_outline_24
},
)
messageSender.send(message)
}
}

View file

@ -178,7 +178,7 @@ private fun BottomSheetIcon(icon: MessageBottomSheetUM.Icon, modifier: Modifier
}
val backgroundColor = when (icon.backgroundType) {
MessageBottomSheetUM.Icon.BackgroundType.Unspecified -> Color.Unspecified
MessageBottomSheetUM.Icon.BackgroundType.Unspecified -> TangemTheme.colors3.bg.tertiary
MessageBottomSheetUM.Icon.BackgroundType.SameAsTint -> {
if (tint == Color.Unspecified) Color.Unspecified else tint.copy(alpha = 0.1f)
}
@ -217,7 +217,7 @@ private fun BottomSheetVector(vector: MessageBottomSheetUM.Vector, modifier: Mod
}
val backgroundColor = when (vector.backgroundType) {
MessageBottomSheetUM.Vector.BackgroundType.Unspecified -> Color.Unspecified
MessageBottomSheetUM.Vector.BackgroundType.Unspecified -> TangemTheme.colors3.bg.tertiary
MessageBottomSheetUM.Vector.BackgroundType.SameAsTint -> tint
MessageBottomSheetUM.Vector.BackgroundType.Accent -> TangemTheme.colors3.bg.status.infoSubtle
MessageBottomSheetUM.Vector.BackgroundType.Informative -> TangemTheme.colors3.bg.status.infoSubtle

View file

@ -41,7 +41,11 @@ fun TangemTopSnackbar(snackbarMessage: SnackbarMessage, modifier: Modifier = Mod
val action = snackbarMessage.action
val hasAction = actionLabel != null && action != null
var isTextOverflowing by remember { mutableStateOf(false) }
var isTextOverflowing by remember(
snackbarMessage.message,
snackbarMessage.startIconId,
hasAction,
) { mutableStateOf(false) }
val shape = if (isTextOverflowing) {
RoundedCornerShape(TangemTheme.dimens2.x5)
} else {
@ -55,7 +59,7 @@ fun TangemTopSnackbar(snackbarMessage: SnackbarMessage, modifier: Modifier = Mod
.clip(shape)
.hazeEffectTangem()
.sizeIn(minHeight = TangemTheme.dimens2.x11)
.padding(start = TangemTheme.dimens2.x5, end = TangemTheme.dimens2.x1)
.padding(start = TangemTheme.dimens2.x5, end = TangemTheme.dimens2.x3)
.padding(vertical = TangemTheme.dimens2.x1),
verticalArrangement = Arrangement.Center,
) {
@ -81,7 +85,10 @@ fun TangemTopSnackbar(snackbarMessage: SnackbarMessage, modifier: Modifier = Mod
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.body2,
onTextLayout = { if (hasAction) isTextOverflowing = it.hasVisualOverflow },
onTextLayout = { layoutResult ->
val isOverflowing = hasAction && layoutResult.hasVisualOverflow
if (isTextOverflowing != isOverflowing) isTextOverflowing = isOverflowing
},
)
SpacerW(TangemTheme.dimens2.x4)

View file

@ -10,14 +10,17 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.innerShadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.shadow.Shadow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.haze.hazeForegroundEffectTangem
import com.tangem.core.ui.extensions.conditionalCompose
@ -232,15 +235,28 @@ enum class TangemMessageEffect(val isAnimatable: Boolean) {
None -> if (isInDarkTheme) Color(0x1AFFFFFF) else Color.Transparent
}
}
/**
* Color of the inner shadow that glows inward evenly from every edge over the card body,
* or `null` when the effect doesn't have one. [None] uses a subtle white 15% highlight
* (Figma: inner shadow X0 Y0 blur 74 spread 0, #FFFFFF · 15%).
*/
@Suppress("MagicNumber")
fun getInnerGlowColor(isInDarkTheme: Boolean): Color? = when (this) {
None -> if (isInDarkTheme) Color(0x26FFFFFF) else null
else -> null
}
}
/** Applies a message effect background to the [Modifier] based on the provided [messageEffect] and [radius] */
@Suppress("LongMethod")
@Composable
fun Modifier.messageEffectBackground(messageEffect: TangemMessageEffect, radius: Dp, contentColor: Color): Modifier {
val isInDarkTheme = LocalIsInDarkTheme.current
val borderGradientColors = remember(messageEffect, isInDarkTheme) { messageEffect.getBorderGradient(isInDarkTheme) }
val gradientColors = remember(messageEffect, isInDarkTheme) { messageEffect.getColorGradient(isInDarkTheme) }
val gradientTint = remember(messageEffect, isInDarkTheme) { messageEffect.getGradientTint(isInDarkTheme) }
val innerGlowColor = remember(messageEffect, isInDarkTheme) { messageEffect.getInnerGlowColor(isInDarkTheme) }
val angle by if (messageEffect.isAnimatable) {
LocalMessageEffectAnimation.current.offsetState
@ -293,14 +309,28 @@ fun Modifier.messageEffectBackground(messageEffect: TangemMessageEffect, radius:
blendMode = BlendMode.SrcIn,
)
}
drawRect(
color = contentColor,
topLeft = Offset(padding, padding),
size = Size(size.width - 2 * padding, size.height - 2 * padding),
)
// None ignores contentColor — its body is the white-10% frosted haze, not a solid fill
if (messageEffect != TangemMessageEffect.None) {
drawRect(
color = contentColor,
topLeft = Offset(padding, padding),
size = Size(size.width - 2 * padding, size.height - 2 * padding),
)
}
drawContent()
}
}
.conditionalCompose(innerGlowColor != null) {
innerShadow(
shape = RoundedCornerShape(0.dp),
shadow = Shadow(
radius = 10.dp,
spread = 10.dp,
color = innerGlowColor ?: Color.Transparent,
offset = DpOffset(0.dp, 0.dp),
),
)
}
}
@Composable

View file

@ -76,7 +76,7 @@ fun BigDecimalFiatFormat.defaultAmount(): BigDecimalFormat = BigDecimalFormat {
)
}
} else {
formatter.format(value)
formatter.format(value.zeroIfRoundsToZero(FIAT_MARKET_DEFAULT_DIGITS))
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
}
}
@ -94,7 +94,7 @@ fun BigDecimalFiatFormatStyled.defaultAmount(spanStyleReference: SpanStyleRefere
val formattingAmount = if (value.isLessThanThreshold()) {
FIAT_FORMAT_THRESHOLD
} else {
value
value.zeroIfRoundsToZero(FIAT_MARKET_DEFAULT_DIGITS)
}
val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
@ -273,6 +273,9 @@ fun BigDecimalFiatFormat.optionalDecimals(): BigDecimalFormat = BigDecimalFormat
private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD
private fun BigDecimal.zeroIfRoundsToZero(scale: Int): BigDecimal =
if (setScale(scale, RoundingMode.HALF_UP).signum() == 0) BigDecimal.ZERO else this
/**
* Returns amount with correct scale
*/

View file

@ -100,6 +100,38 @@ internal class BigDecimalFiatFormatTest {
.isEqualTo("<" + "0,01".addSymbolWithSpaceRight(usdSymbol))
}
@Test
fun `defaultAmount tiny negative rounding to zero is formatted without sign`() {
val testValue = BigDecimal("-0.0001")
val formatted = testValue.format {
fiat(
fiatCurrencyCode = usdCurrencyCode,
fiatCurrencySymbol = usdSymbol,
locale = testLocale,
).defaultAmount()
}
Truth.assertThat(formatted)
.isEqualTo("0.00".addUsdSymbolLeft())
}
@Test
fun `defaultAmount negative rounding away from zero keeps sign`() {
val testValue = BigDecimal("-0.005")
val formatted = testValue.format {
fiat(
fiatCurrencyCode = usdCurrencyCode,
fiatCurrencySymbol = usdSymbol,
locale = testLocale,
).defaultAmount()
}
Truth.assertThat(formatted)
.isEqualTo("-" + "0.01".addUsdSymbolLeft())
}
// === approximateAmount() ===
@Test

View file

@ -307,7 +307,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
preselectedNetworkId: String? = null,
): DefaultEarnComponent.Params = DefaultEarnComponent.Params(
onBackClick = onBack,
onSearchClicked = clickIntents::openSearch,
preselectedEarnType = preselectedEarnType,
preselectedNetworkId = preselectedNetworkId,
)

View file

@ -27,7 +27,10 @@ import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
@ -36,7 +39,7 @@ import com.tangem.domain.models.earn.PreselectedEarnType
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.features.feed.components.feed.FeedBottomSheetRoute
import com.tangem.features.feed.model.earn.EarnModel
import com.tangem.features.feed.ui.components.FeedSearchBar
import com.tangem.features.feed.ui.LocalIsOpenedInBottomSheet
import com.tangem.features.feed.ui.earn.EarnContent
import kotlinx.serialization.Serializable
@ -60,9 +63,13 @@ internal class DefaultEarnComponent(
val background = LocalMainBottomSheetColor.current.value
val state by earnModel.state.collectAsStateWithLifecycle()
if (LocalRedesignEnabled.current) {
FeedSearchBar(
isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED,
feedListSearchBar = state.feedListSearchBar,
TangemTopBar(
title = resourceReference(R.string.earn_title),
type = if (LocalIsOpenedInBottomSheet.current) {
TangemTopBarType.BottomSheet
} else {
TangemTopBarType.Default
},
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
@ -136,7 +143,6 @@ internal class DefaultEarnComponent(
@Serializable
data class Params(
val onBackClick: () -> Unit,
val onSearchClicked: (source: String) -> Unit,
val preselectedEarnType: PreselectedEarnType? = null,
val preselectedNetworkId: String? = null,
)

View file

@ -8,7 +8,6 @@ import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -22,8 +21,8 @@ import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.earn.EarnNetworks
import com.tangem.domain.models.earn.EarnTokenWithCurrency
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.domain.models.earn.PreselectedEarnType
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager.AnalyticsParams.Companion.CategoryEarn
import com.tangem.features.feed.components.earn.DefaultEarnComponent
import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent
@ -400,7 +399,6 @@ internal class EarnModel @Inject constructor(
onNetworkFilterClick = ::onNetworkFilterClick,
onTypeFilterClick = ::onTypeFilterClick,
onScroll = ::onMostlyUsedScrolled,
onSearchBarClicked = { params.onSearchClicked(AnalyticsParam.ScreensSources.Earn.value) },
),
)
}

View file

@ -1,10 +1,8 @@
package com.tangem.features.feed.model.earn.state
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.feed.model.earn.state.transformers.EarnUMTransformer
import com.tangem.features.feed.ui.earn.state.*
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -36,10 +34,6 @@ internal class EarnStateController @Inject constructor() {
onNetworkFilterClick = {},
onTypeFilterClick = {},
onSliderScroll = {},
feedListSearchBar = FeedListSearchBar(
placeholderText = TextReference.EMPTY,
onBarClick = {},
),
)
}
}

View file

@ -1,16 +1,12 @@
package com.tangem.features.feed.model.earn.state.transformers
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.earn.state.EarnUM
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
internal class UpdateEarnUMInitialStateTransformer(
private val onBackClick: () -> Unit,
private val onNetworkFilterClick: () -> Unit,
private val onTypeFilterClick: () -> Unit,
private val onScroll: () -> Unit,
private val onSearchBarClicked: () -> Unit,
) : EarnUMTransformer {
override fun transform(prevState: EarnUM): EarnUM {
@ -19,10 +15,6 @@ internal class UpdateEarnUMInitialStateTransformer(
onNetworkFilterClick = onNetworkFilterClick,
onTypeFilterClick = onTypeFilterClick,
onSliderScroll = onScroll,
feedListSearchBar = FeedListSearchBar(
onBarClick = onSearchBarClicked,
placeholderText = resourceReference(id = R.string.markets_search_title_placeholder),
),
)
}
}

View file

@ -22,12 +22,14 @@ import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.*
import com.tangem.domain.models.earn.EarnType
import com.tangem.features.feed.ui.earn.components.*
import com.tangem.features.feed.ui.earn.state.*
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
import kotlinx.collections.immutable.persistentListOf
private const val EARN_LOAD_MORE_BUFFER = 3
@ -663,10 +665,6 @@ private fun previewEarnUM(
onNetworkFilterClick = {},
onTypeFilterClick = {},
onSliderScroll = {},
feedListSearchBar = FeedListSearchBar(
placeholderText = TextReference.Str("Search tokens & news"),
onBarClick = {},
),
)
private const val PLACEHOLDER_ITEMS_COUNT = 8

View file

@ -1,7 +1,6 @@
package com.tangem.features.feed.ui.earn.state
import androidx.compose.runtime.Immutable
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
@Immutable
internal data class EarnUM(
@ -12,5 +11,4 @@ internal data class EarnUM(
val onNetworkFilterClick: () -> Unit,
val onTypeFilterClick: () -> Unit,
val onSliderScroll: () -> Unit,
val feedListSearchBar: FeedListSearchBar,
)