Updated on 2026-08-14
This commit is contained in:
parent
9cde8a7ba0
commit
183cb93c1a
13 changed files with 107 additions and 46 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue