Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-02 19:19:33 +07:00
commit c3214d41f8
25 changed files with 391 additions and 187 deletions

View file

@ -691,9 +691,7 @@ internal class ChildFactory @Inject constructor(
deeplink = mode.deeplink,
)
is AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings -> FromBannerInSettings
is AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain -> FromBannerOnMain(
userWalletId = mode.userWalletId,
)
is AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain -> FromBannerOnMain
},
componentFactory = tangemPayOnboardingComponentFactory,
)

View file

@ -444,9 +444,7 @@ sealed class AppRoute(val path: String) : Route {
) : Mode()
@Serializable
data class FromBannerOnMain(
val userWalletId: UserWalletId,
) : Mode()
data object FromBannerOnMain : Mode()
@Serializable
data object FromBannerInSettings : Mode()

View file

@ -2,7 +2,10 @@ package com.tangem.common.ui.news
import android.content.res.Configuration
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CardColors
import androidx.compose.material3.Text
@ -11,6 +14,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -21,15 +26,12 @@ import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerHMax
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
@ -65,52 +67,61 @@ private fun TrendingArticle(
colors: CardColors,
modifier: Modifier = Modifier,
) {
Column(
Box(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(colors.containerColor)
.clickable { onArticleClick() }
.padding(vertical = 24.dp, horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
.clickable { onArticleClick() },
) {
Text(
modifier = Modifier
.background(
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
shape = RoundedCornerShape(8.dp),
)
.padding(horizontal = 8.dp, vertical = 4.dp),
text = stringResourceSafe(R.string.feed_trending_now),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.icon.accent,
Image(
modifier = Modifier.matchParentSize(),
painter = painterResource(R.drawable.bg_trending_article),
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
Column(
modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier
.background(
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
shape = RoundedCornerShape(8.dp),
)
.padding(horizontal = 8.dp, vertical = 4.dp),
text = stringResourceSafe(R.string.feed_trending_now),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.icon.accent,
)
SpacerH(12.dp)
SpacerH(12.dp)
Text(
text = articleConfigUM.title,
color = if (articleConfigUM.isViewed) {
TangemTheme.colors.text.tertiary
} else {
TangemTheme.colors.text.primary1
},
style = TangemTheme.typography.h3,
textAlign = TextAlign.Center,
)
Text(
text = articleConfigUM.title,
color = if (articleConfigUM.isViewed) {
TangemTheme.colors.text.tertiary
} else {
TangemTheme.colors.text.primary1
},
style = TangemTheme.typography.h3,
textAlign = TextAlign.Center,
)
SpacerH(8.dp)
SpacerH(8.dp)
ArticleInfo(
score = articleConfigUM.score,
createdAt = articleConfigUM.createdAt.resolveReference(),
)
ArticleInfo(
score = articleConfigUM.score,
createdAt = articleConfigUM.createdAt.resolveReference(),
)
SpacerH(32.dp)
SpacerH(32.dp)
Tags(
modifier = Modifier.padding(horizontal = 46.dp),
tags = articleConfigUM.tags.toImmutableList(),
)
Tags(
modifier = Modifier.padding(horizontal = 46.dp),
tags = articleConfigUM.tags.toImmutableList(),
)
}
}
}
@ -182,46 +193,14 @@ private fun DefaultArticle(
overflow = TextOverflow.Ellipsis,
)
SpacerHMax(modifier = Modifier.padding(bottom = 16.dp))
SpacerH(16.dp)
SpacerHMax()
Tags(tags = articleConfigUM.tags.toImmutableList())
}
}
@Composable
private fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
FlowRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
maxItemsInEachRow = MAX_TAGS_COUNT_TO_SHOW + 1,
maxLines = 1,
itemVerticalAlignment = Alignment.Bottom,
) {
val displayedTags = if (tags.size > MAX_TAGS_COUNT_TO_SHOW) {
tags.take(MAX_TAGS_COUNT_TO_SHOW)
} else {
tags
}
displayedTags.forEach { tag ->
Label(state = tag)
}
if (tags.size > MAX_TAGS_COUNT_TO_SHOW) {
val remainingItems = tags.size - MAX_TAGS_COUNT_TO_SHOW
Label(
state = LabelUM(
text = TextReference.Str("${StringsSigns.PLUS}$remainingItems"),
maxLines = 1,
),
)
}
}
}
private const val MAX_TAGS_COUNT_TO_SHOW = 3
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable

View file

@ -0,0 +1,128 @@
package com.tangem.common.ui.news
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.layout.SubcomposeMeasureScope
import androidx.compose.ui.unit.Constraints
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.extensions.TextReference
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.ImmutableList
private val TAG_SPACING = 4.dp
/**
* A layout that displays a list of tags, automatically calculating how many can fit
* in a single line. If not all tags fit, it shows an overflow indicator (e.g., "+N").
*
* @param tags The immutable list of [LabelUM] to display.
* @param modifier The modifier to be applied to the layout.
*/
@Composable
internal fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
if (tags.isEmpty()) return
SubcomposeLayout(modifier = modifier) { constraints ->
val spacingPx = TAG_SPACING.roundToPx()
val tagPlaceables = subcompose(ContentSlot.Tags) {
tags.forEach { tag ->
Label(state = tag)
}
}.map { it.measure(constraints) }
val layoutInfo = calculateLayoutInfo(
maxWidth = constraints.maxWidth,
spacingPx = spacingPx,
tagPlaceables = tagPlaceables,
)
val overflowPlaceable = layoutInfo.overflowCount.takeIf { it > 0 }?.let {
subcompose(ContentSlot.Overflow) {
OverflowLabel(count = it)
}.first().measure(constraints)
}
val visiblePlaceables = tagPlaceables.take(layoutInfo.visibleCount)
val allVisibleItems = visiblePlaceables + listOfNotNull(overflowPlaceable)
val width = calculateRowWidth(
placeables = allVisibleItems,
spacingPx = spacingPx,
).coerceIn(constraints.minWidth, constraints.maxWidth)
val height = allVisibleItems.maxOfOrNull { it.height } ?: 0
layout(width = width, height = height) {
var xPosition = 0
allVisibleItems.forEachIndexed { index, placeable ->
if (index > 0) {
xPosition += spacingPx
}
val yPosition = height - placeable.height
placeable.placeRelative(xPosition, yPosition)
xPosition += placeable.width
}
}
}
}
/**
* Calculates how many tags can be displayed and how many are overflowing.
*/
private fun SubcomposeMeasureScope.calculateLayoutInfo(
maxWidth: Int,
spacingPx: Int,
tagPlaceables: List<Placeable>,
): TagsLayoutInfo {
val totalWidthWithoutOverflow = calculateRowWidth(tagPlaceables, spacingPx)
if (totalWidthWithoutOverflow <= maxWidth) {
return TagsLayoutInfo(visibleCount = tagPlaceables.size, overflowCount = 0)
}
for (count in tagPlaceables.indices.reversed()) {
val visibleTags = tagPlaceables.take(count)
val overflowCount = tagPlaceables.size - count
val tempOverflowPlaceable = subcompose("overflow_temp_$overflowCount") {
OverflowLabel(count = overflowCount)
}.first().measure(Constraints())
val widthWithOverflow = calculateRowWidth(
placeables = visibleTags + tempOverflowPlaceable,
spacingPx = spacingPx,
)
if (widthWithOverflow <= maxWidth) {
return TagsLayoutInfo(visibleCount = count, overflowCount = overflowCount)
}
}
return TagsLayoutInfo(visibleCount = 0, overflowCount = tagPlaceables.size)
}
@Composable
private fun OverflowLabel(count: Int) {
Label(
state = LabelUM(
text = TextReference.Str("${StringsSigns.PLUS}$count"),
maxLines = 1,
),
)
}
private fun calculateRowWidth(placeables: List<Placeable>, spacingPx: Int): Int {
if (placeables.isEmpty()) return 0
val tagsWidth = placeables.sumOf { it.width }
val spacingWidth = spacingPx * (placeables.size - 1).coerceAtLeast(0)
return tagsWidth + spacingWidth
}
private data class TagsLayoutInfo(val visibleCount: Int, val overflowCount: Int)
private enum class ContentSlot { Tags, Overflow }

View file

@ -120,7 +120,7 @@ fun TokenItem(
composables.icon.invoke(
Modifier
.layoutId(layoutId = LayoutId.ICON)
.padding(end = TangemTheme.dimens.spacing8)
.padding(end = TangemTheme.dimens.spacing12)
.testTag(TokenElementsTestTags.TOKEN_ICON),
)
} else {
@ -128,7 +128,7 @@ fun TokenItem(
state = state.iconState,
modifier = Modifier
.layoutId(layoutId = LayoutId.ICON)
.padding(end = TangemTheme.dimens.spacing8)
.padding(end = TangemTheme.dimens.spacing12)
.testTag(TokenElementsTestTags.TOKEN_ICON),
)
}

View file

@ -7,8 +7,10 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalRippleConfiguration
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -32,6 +34,7 @@ import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.internal.GroupTitleItem
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.ProvideSharedTransitionScope
@ -71,13 +74,13 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea
ProvideSharedTransitionScope(modifier) {
val iconSharedContentState = rememberSharedContentState(key = "icon_${state.id}")
val titleSharedContentState = rememberSharedContentState(key = "title_${state.id}")
val boundsTransform = BoundsTransform { _, _ -> tween(350) }
val boundsTransform = BoundsTransform { _, _ -> tween(250) }
AnimatedContent(
targetState = state.isExpanded,
transitionSpec = {
fadeIn(animationSpec = tween(350, delayMillis = 90))
.togetherWith(fadeOut(animationSpec = tween(350)))
fadeIn(animationSpec = tween(250, delayMillis = 90))
.togetherWith(fadeOut(animationSpec = tween(250)))
},
) { isExpanded ->
val animatedContentScope = this
@ -116,7 +119,7 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea
val textSize by animateFloatAsState(
targetValue = textStyle.fontSize.value,
animationSpec = tween(350),
animationSpec = tween(250),
)
TokenTitle(
@ -133,32 +136,34 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea
},
)
if (isExpanded) {
ExpandedPortfolioHeader(
state = state.tokenItemUM,
isCollapsable = state.isCollapsable,
composables = composables,
balanceTextModifier = Modifier
.animateEnterExit(
enter = fadeIn(tween(350)) +
slideInHorizontally(
tween(350),
initialOffsetX = { fullWidth -> fullWidth * 2 },
),
exit = fadeOut(
tween(350),
) + slideOutHorizontally(
tween(350),
targetOffsetX = { fullWidth -> fullWidth * 2 },
CompositionLocalProvider(LocalRippleConfiguration provides null) {
if (isExpanded) {
ExpandedPortfolioHeader(
state = state.tokenItemUM,
isCollapsable = state.isCollapsable,
isBalanceHidden = isBalanceHidden,
composables = composables,
balanceTextModifier = Modifier
.animateEnterExit(
enter = fadeIn(tween(250)) +
slideInHorizontally(
tween(250),
initialOffsetX = { fullWidth -> fullWidth },
),
exit = fadeOut(tween(250)) +
slideOutHorizontally(
tween(250),
targetOffsetX = { fullWidth -> fullWidth },
),
),
),
)
} else {
TokenItem(
state = state.tokenItemUM,
isBalanceHidden = isBalanceHidden,
composables = composables,
)
)
} else {
TokenItem(
state = state.tokenItemUM,
isBalanceHidden = isBalanceHidden,
composables = composables,
)
}
}
}
}
@ -180,6 +185,7 @@ fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: B
@Composable
fun ExpandedPortfolioHeader(
state: TokenItemState,
isBalanceHidden: Boolean,
isCollapsable: Boolean,
composables: TokenItemComposables?,
modifier: Modifier = Modifier,
@ -191,8 +197,10 @@ fun ExpandedPortfolioHeader(
.fillMaxWidth()
.clickable(onClick = { state.onItemClick?.invoke(state) })
.padding(
vertical = if (composables != null) 8.dp else 4.dp,
horizontal = 12.dp,
top = 12.dp,
bottom = 4.dp,
start = 12.dp,
end = 12.dp,
),
) {
if (composables != null) {
@ -227,12 +235,19 @@ fun ExpandedPortfolioHeader(
modifier = Modifier.weight(1f),
) {
if (composables != null) {
composables.title.invoke(Modifier.alignByBaseline())
composables.title.invoke(
Modifier
.padding(vertical = 2.dp)
.alignByBaseline(),
)
} else {
when (val titleState = state.titleState) {
TokenItemState.TitleState.Loading -> Unit
TokenItemState.TitleState.Locked -> Unit
is TokenItemState.TitleState.Content -> Text(
modifier = Modifier
.padding(vertical = 2.dp)
.alignByBaseline(),
text = titleState.text.resolveReference(),
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
@ -249,10 +264,10 @@ fun ExpandedPortfolioHeader(
}
Text(
text = text.orEmpty(),
text = text.orEmpty().orMaskWithStars(isBalanceHidden),
modifier = balanceTextModifier
.alignByBaseline()
.padding(horizontal = 8.dp),
.padding(horizontal = 4.dp),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption1,
overflow = TextOverflow.Ellipsis,

View file

@ -1,5 +1,9 @@
package com.tangem.core.ui.utils
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.BoundsTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.layout.Box
@ -7,6 +11,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
@Composable
fun TangemSharedTransitionLayout(
@ -31,6 +42,78 @@ fun ProvideSharedTransitionScope(modifier: Modifier = Modifier, content: @Compos
}
}
private val LocalSharedTransitionScope = staticCompositionLocalOf<SharedTransitionScope> {
error("No SharedTransitionScope provided")
private val LocalSharedTransitionScope = staticCompositionLocalOf<SharedTransitionScope> { StubSharedTransitionScope() }
private class StubSharedTransitionScope : SharedTransitionScope {
override val isTransitionActive: Boolean = false
override val Placeable.PlacementScope.lookaheadScopeCoordinates: LayoutCoordinates
get() = error("No LookaheadScopeCoordinates available in StubSharedTransitionScope")
override fun Modifier.skipToLookaheadSize(enabled: () -> Boolean): Modifier {
return this
}
override fun Modifier.renderInSharedTransitionScopeOverlay(
zIndexInOverlay: Float,
renderInOverlay: () -> Boolean,
): Modifier {
return this
}
override fun Modifier.sharedElement(
sharedContentState: SharedTransitionScope.SharedContentState,
animatedVisibilityScope: AnimatedVisibilityScope,
boundsTransform: BoundsTransform,
placeholderSize: SharedTransitionScope.PlaceholderSize,
renderInOverlayDuringTransition: Boolean,
zIndexInOverlay: Float,
clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip,
): Modifier {
return this
}
override fun Modifier.sharedBounds(
sharedContentState: SharedTransitionScope.SharedContentState,
animatedVisibilityScope: AnimatedVisibilityScope,
enter: EnterTransition,
exit: ExitTransition,
boundsTransform: BoundsTransform,
resizeMode: SharedTransitionScope.ResizeMode,
placeholderSize: SharedTransitionScope.PlaceholderSize,
renderInOverlayDuringTransition: Boolean,
zIndexInOverlay: Float,
clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip,
): Modifier {
return this
}
override fun Modifier.sharedElementWithCallerManagedVisibility(
sharedContentState: SharedTransitionScope.SharedContentState,
visible: Boolean,
boundsTransform: BoundsTransform,
placeholderSize: SharedTransitionScope.PlaceholderSize,
renderInOverlayDuringTransition: Boolean,
zIndexInOverlay: Float,
clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip,
): Modifier {
return this
}
override fun OverlayClip(clipShape: Shape): SharedTransitionScope.OverlayClip {
return object : SharedTransitionScope.OverlayClip {
override fun getClipPath(
sharedContentState: SharedTransitionScope.SharedContentState,
bounds: Rect,
layoutDirection: LayoutDirection,
density: Density,
): Path? {
return Path()
}
}
}
override fun LayoutCoordinates.toLookaheadCoordinates(): LayoutCoordinates {
return this
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -12,8 +12,7 @@ import kotlinx.serialization.Serializable
data class AvailableToAddData(
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet>,
) {
val isAvailableToAdd: Boolean
get() = availableToAddWallets.isNotEmpty()
val isAvailableToAdd: Boolean = availableToAddWallets.values.any { item -> item.isAvailableToAdd }
val isSinglePortfolio: Boolean
get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1
}
@ -23,7 +22,9 @@ data class AvailableToAddWallet(
val accounts: List<AccountStatus>,
val availableNetworks: Set<TokenMarketInfo.Network>,
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount>,
)
) {
val isAvailableToAdd: Boolean = availableToAddAccounts.values.any { item -> item.isAvailableToAdd }
}
@Serializable
data class AvailableToAddAccount(
@ -38,6 +39,8 @@ data class AvailableToAddAccount(
.filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } }
.toSet()
val isAvailableToAdd: Boolean = availableToAddNetworks.isNotEmpty()
val addedMarketNetworks: Set<TokenMarketInfo.Network> = availableNetworks
.filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } }
.toSet()

View file

@ -70,7 +70,6 @@ internal class AvailableToAddDataConverter @Inject constructor(
val availableToAddAccount = accountStatus.getAvailableToAddAccount(wallet) ?: return@mapNotNull null
accountStatus.account.accountId to availableToAddAccount
}
.filter { (_, account) -> account.availableToAddNetworks.isNotEmpty() }
.toMap()
return AvailableToAddWallet(
userWallet = wallet,

View file

@ -344,9 +344,10 @@ internal class AddToPortfolioModel @Inject constructor(
portfolioSelectorController.isEnabled.value = isEnabled@{ userWallet, accountStatus ->
val availableWallet = state.availableToAddData.availableToAddWallets[userWallet.walletId]
?: return@isEnabled false
val availableAccount =
val isAvailableAccount =
availableWallet.availableToAddAccounts[accountStatus.account.accountId]
return@isEnabled availableAccount != null
?.isAvailableToAdd == true
return@isEnabled isAvailableAccount
}
AddToPortfolioManager.State.Init,
AddToPortfolioManager.State.NothingToAdd,

View file

@ -25,15 +25,19 @@ internal class UpdateGlobalFeedStateTransformer(
prevState.news.newsUMState == NewsUMState.ERROR
val hasLoadingInCharts = loadingStatesByOrder.values.any { it }
val hasErrorInCharts = errorStatesByOrder.values.any { it != null }
val hasAllErrorInCharts = errorStatesByOrder.values.isNotEmpty() && errorStatesByOrder.values.all { it != null }
val hasErrorInMarketChart = errorStatesByOrder[SortByTypeUM.Rating] != null
val hasErrorInMarketPulseChart = errorStatesByOrder
.any { it.key != SortByTypeUM.Rating && it.value != null }
val areAllChartsLoading = loadingStatesByOrder.values.isNotEmpty() && loadingStatesByOrder.values.all { it }
val areAllChartsError = hasAllErrorInCharts || hasErrorInCharts && hasLoadingInCharts
val areAllChartsError = when {
hasErrorInMarketChart && hasErrorInMarketPulseChart -> true
hasErrorInMarketPulseChart && hasLoadingInCharts -> true
hasErrorInMarketChart && hasLoadingInCharts -> true
else -> false
}
val newGlobalState = when {
isNewsLoading && areAllChartsError -> GlobalFeedState.Loading
isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading
isNewsError && areAllChartsLoading -> GlobalFeedState.Loading
isNewsError && areAllChartsError -> {
if (previousGlobalState !is GlobalFeedState.Error) {
@ -43,18 +47,11 @@ internal class UpdateGlobalFeedStateTransformer(
onRetryClicked = onRetryClicked,
)
}
isNewsLoading && areAllChartsError -> GlobalFeedState.Loading
isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading
else -> GlobalFeedState.Content
}
return prevState.copy(
globalState = newGlobalState,
news = prevState.news.copy(
newsUMState = when {
isNewsError -> NewsUMState.ERROR
else -> NewsUMState.CONTENT
},
),
)
return prevState.copy(globalState = newGlobalState)
}
private fun sendErrorAnalytics() {

View file

@ -423,7 +423,7 @@ private fun Charts(
) {
BlockCard(
modifier = modifier,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.primary),
) {
Column(modifier = Modifier.fillMaxWidth()) {
when (marketChart) {

View file

@ -12,8 +12,7 @@ import kotlinx.serialization.Serializable
internal data class AvailableToAddData(
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet>,
) {
val isAvailableToAdd: Boolean
get() = availableToAddWallets.isNotEmpty()
val isAvailableToAdd: Boolean = availableToAddWallets.values.any { item -> item.isAvailableToAdd }
val isSinglePortfolio: Boolean
get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1
}
@ -23,7 +22,9 @@ internal data class AvailableToAddWallet(
val accounts: List<AccountStatus>,
val availableNetworks: Set<TokenMarketInfo.Network>,
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount>,
)
) {
val isAvailableToAdd: Boolean = availableToAddAccounts.values.any { item -> item.isAvailableToAdd }
}
@Serializable
internal data class AvailableToAddAccount(
@ -31,6 +32,7 @@ internal data class AvailableToAddAccount(
val availableNetworks: Set<TokenMarketInfo.Network>,
val addedNetworks: Set<Network>,
) {
val isSingleNetwork: Boolean
get() = availableNetworks.size == 1
@ -38,6 +40,8 @@ internal data class AvailableToAddAccount(
.filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } }
.toSet()
val isAvailableToAdd: Boolean = availableToAddNetworks.isNotEmpty()
val addedMarketNetworks: Set<TokenMarketInfo.Network> = availableNetworks
.filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } }
.toSet()

View file

@ -70,7 +70,6 @@ internal class AvailableToAddDataConverter @Inject constructor(
val availableToAddAccount = accountStatus.getAvailableToAddAccount(wallet) ?: return@mapNotNull null
accountStatus.account.accountId to availableToAddAccount
}
.filter { (_, account) -> account.availableToAddNetworks.isNotEmpty() }
.toMap()
return AvailableToAddWallet(
userWallet = wallet,

View file

@ -334,9 +334,10 @@ internal class AddToPortfolioModel @Inject constructor(
portfolioSelectorController.isEnabled.value = isEnabled@{ userWallet, accountStatus ->
val availableWallet = state.availableToAddData.availableToAddWallets[userWallet.walletId]
?: return@isEnabled false
val availableAccount =
val isAvailableAccount =
availableWallet.availableToAddAccounts[accountStatus.account.accountId]
return@isEnabled availableAccount != null
?.isAvailableToAdd == true
return@isEnabled isAvailableAccount
}
AddToPortfolioManager.State.Init,
AddToPortfolioManager.State.NothingToAdd,

View file

@ -1536,9 +1536,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
swapAmount = amount,
quotesLoadedState = swapState,
isAllowedToSpend = isAllowedToSpend,
spenderAddress = requireNotNull(quoteModel.allowanceContract) {
"allowanceContract is required for DEX"
},
spenderAddress = quoteModel.allowanceContract,
)
if (state !is SwapState.QuotesLoadedState) return state
state.copy(
@ -2081,14 +2079,14 @@ internal class SwapInteractorImpl @AssistedInject constructor(
) ?: TxFeeState.Empty
}
@Suppress("LongParameterList", "LongMethod")
@Suppress("LongParameterList", "LongMethod", "CanBeNonNullable")
private suspend fun updatePermissionState(
networkId: String,
fromTokenStatus: CryptoCurrencyStatus,
fromAccount: Account.CryptoPortfolio?,
swapAmount: SwapAmount,
quotesLoadedState: SwapState.QuotesLoadedState,
spenderAddress: String,
spenderAddress: String?,
isAllowedToSpend: Boolean,
): SwapState {
val fromToken = fromTokenStatus.currency
@ -2111,7 +2109,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
}
// setting up amount for approve with given amount for swap [SwapApproveType.Limited]
val callData = SmartContractCallDataProviderFactory.getApprovalCallData(
spenderAddress = spenderAddress,
spenderAddress = requireNotNull(spenderAddress) { "spenderAddress cant be null" },
amount = swapAmount.value.convertToSdkAmount(fromTokenStatus),
blockchain = fromToken.network.toBlockchain(),
)

View file

@ -2,9 +2,7 @@ package com.tangem.feature.swap
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
@ -103,13 +101,9 @@ internal class DefaultSwapComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
if (sendFeatureToggles.isGaslessTransactionsEnabled) {
val fromCryptoCurrency = if (model.isOrderReversed) {
model.dataState.toCryptoCurrency
} else {
model.dataState.fromCryptoCurrency
}
val feePaidCryptoCurrency = model.dataState.feePaidCryptoCurrency
val amount = model.dataState.amount
val fromCryptoCurrency by remember { derivedStateOf { model.dataState.fromCryptoCurrency } }
val feePaidCryptoCurrency by remember { derivedStateOf { model.dataState.feePaidCryptoCurrency } }
val amount by remember { derivedStateOf { model.dataState.amount } }
LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, amount.isNullOrBlank()) {
if (amount.isNullOrBlank()) {

View file

@ -220,9 +220,17 @@ internal class SwapModel @Inject constructor(
private val swapRouter: SwapRouter = SwapRouter(router = router)
private var userCountry: UserCountry? = null
private lateinit var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus
private var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null
private var toAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null
/**
* If accountsFeatureToggles is off OR user came from Tangem Pay -> fromAccountCurrencyStatus == null
* If accountsFeatureToggles is on AND user didn't come from Tangem Pay -> fromAccountCurrencyStatus != null
*
* Remove when accounts are integrated into Tangem Pay
*/
private val canUseFromAccountCurrencyStatus = accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null
private val isUserResolvableError: (SwapState) -> Boolean = { swapState ->
swapState is SwapState.SwapError &&
(
@ -288,7 +296,7 @@ internal class SwapModel @Inject constructor(
}
modelScope.launch(dispatchers.io) {
if (accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null) {
if (canUseFromAccountCurrencyStatus) {
isAccountsMode = isAccountsModeEnabledUseCase.invokeSync()
val fromAccountStatus = getAccountCurrencyStatusUseCase.invokeSync(
@ -579,11 +587,11 @@ internal class SwapModel @Inject constructor(
} else {
initialFromStatus to selectedCurrency
}
val (fromAccount, toAccount) = if (accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null) {
val (fromAccount, toAccount) = if (canUseFromAccountCurrencyStatus) {
if (isOrderReversed) {
selectedAccount to fromAccountCurrencyStatus.account
selectedAccount to requireNotNull(fromAccountCurrencyStatus).account
} else {
fromAccountCurrencyStatus.account to selectedAccount
requireNotNull(fromAccountCurrencyStatus).account to selectedAccount
}
} else {
null to null
@ -1266,7 +1274,7 @@ internal class SwapModel @Inject constructor(
fromAccount = foundAccount
toToken = initialFromStatus
toAccount = if (accountsFeatureToggles.isFeatureEnabled) {
fromAccountCurrencyStatus.account
fromAccountCurrencyStatus?.account
} else {
null
}
@ -1284,7 +1292,7 @@ internal class SwapModel @Inject constructor(
} else {
fromToken = initialFromStatus
fromAccount = if (accountsFeatureToggles.isFeatureEnabled) {
fromAccountCurrencyStatus.account
fromAccountCurrencyStatus?.account
} else {
null
}

View file

@ -16,9 +16,7 @@ interface TangemPayOnboardingComponent : ComposableContentComponent {
val userWalletId: UserWalletId,
) : Params()
data class FromBannerOnMain(
val userWalletId: UserWalletId,
) : Params()
data object FromBannerOnMain : Params()
data object FromBannerInSettings : Params()
}

View file

@ -131,13 +131,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
is TangemPayOnboardingComponent.Params.ContinueOnboarding -> {
checkCustomerInfo(params.userWalletId)
}
is TangemPayOnboardingComponent.Params.FromBannerOnMain -> {
if (eligibleWalletsIds.any { walletId -> walletId == params.userWalletId }) {
checkCustomerInfo(userWalletId = params.userWalletId)
} else {
openWalletSelectorIfNeeds(eligibleWalletsIds)
}
}
is TangemPayOnboardingComponent.Params.FromBannerOnMain,
is TangemPayOnboardingComponent.Params.Deeplink,
is TangemPayOnboardingComponent.Params.FromBannerInSettings,
-> {

View file

@ -243,7 +243,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
modelScope.launch {
val isEligible = tangemPayEligibilityManager.getTangemPayAvailability()
if (isEligible) {
router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain(userWalletId))
router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain)
} else {
stateHolder.update(transformer = TangemPayHideOnboardingStateTransformer(userWalletId))
}

View file

@ -219,9 +219,8 @@ private fun LazyItemScope.DraggableItem(
state = item.tokenItemState,
isCollapsable = false,
composables = null,
modifier = modifierWithBackground
.padding(top = 8.dp)
.fillMaxWidth(),
isBalanceHidden = isBalanceHidden,
modifier = modifierWithBackground.fillMaxWidth(),
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
import androidx.compose.animation.*
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
@ -141,7 +142,7 @@ private fun LazyListScope.portfolioItem(
lastIndexProxy = lastIndex
return@LaunchedEffect
}
delay(timeMillis = minOf(50 * tokens.size, 350).toLong())
delay(timeMillis = minOf(50 * tokens.size, 250).toLong())
lastIndexProxy = 0
}
@ -179,13 +180,13 @@ private fun SlideInItemVisibility(
enter = fadeIn(
tween(100, delayMillis = delayEnter),
) + expandVertically(
tween(100, delayMillis = delayEnter),
tween(100, delayMillis = delayEnter, easing = FastOutLinearInEasing),
expandFrom = Alignment.Top,
),
exit = fadeOut(
tween(100, delayMillis = delayExit),
) + shrinkVertically(
tween(100, delayMillis = delayExit),
tween(100, delayMillis = delayExit, easing = FastOutLinearInEasing),
shrinkTowards = Alignment.Top,
),
) {

View file

@ -24,6 +24,7 @@ import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetContractAddressUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyPendingTracker
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory
@ -59,6 +60,7 @@ internal class YieldSupplyApproveModel @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val yieldSupplyGetContractAddressUseCase: YieldSupplyGetContractAddressUseCase,
private val yieldSupplyPendingTracker: YieldSupplyPendingTracker,
private val yieldSupplyAlertFactory: YieldSupplyAlertFactory,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
@ -159,7 +161,12 @@ internal class YieldSupplyApproveModel @Inject constructor(
)
params.callback.onTransactionProgress(false)
},
ifRight = {
ifRight = { txHash ->
yieldSupplyPendingTracker.addPending(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,
txIds = listOf(txHash),
)
val event = AnalyticsParam.TxSentFrom.Earning(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,