Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-07 13:24:17 +05:00
parent 14726d2fb4
commit 5b031d5c0e
15 changed files with 448 additions and 70 deletions

View file

@ -18,6 +18,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent
import com.tangem.features.commonfeatures.impl.R
@ -69,7 +70,7 @@ internal fun AddToPortfolioBottomSheet(
AddToPortfolioRoutes.AddToken,
AddToPortfolioRoutes.Empty,
is AddToPortfolioRoutes.NetworkSelector,
AddToPortfolioRoutes.TokenActions,
is AddToPortfolioRoutes.TokenActions,
-> true
}
if (isScrollableContent) {
@ -92,11 +93,12 @@ private fun AddToPortfolioBottomSheetTitle(
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val title: TextReference = when (stack.active.configuration) {
val title: TextReference = when (val config = stack.active.configuration) {
AddToPortfolioRoutes.AddToken -> resourceReference(R.string.common_add_token)
AddToPortfolioRoutes.Empty -> TextReference.EMPTY
is AddToPortfolioRoutes.NetworkSelector -> resourceReference(R.string.common_choose_network)
AddToPortfolioRoutes.TokenActions -> resourceReference(R.string.common_get_token)
is AddToPortfolioRoutes.TokenActions ->
resourceReference(R.string.get_token_title, wrappedList(config.currencyName))
AddToPortfolioRoutes.UserPortfolio -> resourceReference(R.string.markets_portfolio_block_title)
AddToPortfolioRoutes.PortfolioSelector -> (stack.active.instance as PortfolioSelectorComponent)
.title.collectAsStateWithLifecycle().value

View file

@ -104,7 +104,7 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
): ComposableContentComponent = when (config) {
AddToPortfolioRoutes.AddToken -> addTokenComponent
AddToPortfolioRoutes.PortfolioSelector -> portfolioSelectorComponent
AddToPortfolioRoutes.TokenActions -> tokenActionsComponent
is AddToPortfolioRoutes.TokenActions -> tokenActionsComponent
AddToPortfolioRoutes.Empty -> ComposableContentComponent.EMPTY
AddToPortfolioRoutes.UserPortfolio -> createUserPortfolioComponent(componentContext)
is AddToPortfolioRoutes.NetworkSelector -> chooseNetworkComponentFactory.create(

View file

@ -305,7 +305,9 @@ internal class AddToPortfolioModel @Inject constructor(
setupTokenActionsFlow(selectedPortfolioSnapshot, addedToken)
.onEach { cryptoCurrencyData ->
tokenActionsData.emit(cryptoCurrencyData)
navigation.replaceAll(AddToPortfolioRoutes.TokenActions)
navigation.replaceAll(
AddToPortfolioRoutes.TokenActions(cryptoCurrencyData.status.currency.name),
)
}
.onEmpty { finishSuccessFlow(result) }
.launchIn(this)

View file

@ -2,6 +2,7 @@ package com.tangem.features.commonfeatures.impl.addtoportfolio.model
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.commonfeatures.impl.R
internal data class AddToPortfolioRouteUiSpec(
@ -42,8 +43,8 @@ internal fun AddToPortfolioRoutes.uiSpec(): AddToPortfolioRouteUiSpec = when (th
shouldApplyHorizontalPadding = false,
footer = AddToPortfolioFooterKind.UserPortfolioAdd,
)
AddToPortfolioRoutes.TokenActions -> AddToPortfolioRouteUiSpec(
title = resourceReference(R.string.common_get_token),
is AddToPortfolioRoutes.TokenActions -> AddToPortfolioRouteUiSpec(
title = resourceReference(R.string.get_token_title, wrappedList(currencyName)),
isScrollable = false,
shouldApplyHorizontalPadding = true,
footer = AddToPortfolioFooterKind.None,

View file

@ -27,5 +27,5 @@ internal sealed interface AddToPortfolioRoutes : Route {
data object UserPortfolio : AddToPortfolioRoutes
@Serializable
data object TokenActions : AddToPortfolioRoutes
data class TokenActions(val currencyName: String) : AddToPortfolioRoutes
}

View file

@ -17,18 +17,23 @@ import com.tangem.features.commonfeatures.api.addtoportfolio.SelectedPortfolio
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.addtoportfolio.AddTokenComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddTokenUiBuilder.Companion.toggleProgress
import com.tangem.features.wallet.utils.UserWalletImageFetcher
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@ModelScoped
@Suppress("LongParameterList")
@OptIn(ExperimentalCoroutinesApi::class)
internal class AddTokenModel @Inject constructor(
paramsContainer: ParamsContainer,
private val walletImageFetcher: UserWalletImageFetcher,
private val uiBuilder: AddTokenUiBuilder,
private val messageSender: UiMessageSender,
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
@ -46,15 +51,22 @@ internal class AddTokenModel @Inject constructor(
field = MutableStateFlow(value = null)
init {
val walletImageFlow = params.selectedPortfolio
.map { it.userWallet }
.distinctUntilChanged()
.flatMapLatest { walletImageFetcher.walletImage(it, ArtworkSize.SMALL) }
combine(
flow = params.selectedNetwork.distinctUntilChanged(),
flow2 = params.selectedPortfolio.distinctUntilChanged(),
transform = { selectedNetwork, selectedPortfolio ->
flow3 = walletImageFlow,
transform = { selectedNetwork, selectedPortfolio, walletImage ->
addTokenJob.join()
val isTangemIconVisible = needColdWalletInteraction(selectedNetwork, selectedPortfolio)
uiBuilder.updateContent(
selectedPortfolio = selectedPortfolio,
selectedNetwork = selectedNetwork,
walletImage = walletImage,
isTangemIconVisible = isTangemIconVisible,
onConfirmClick = { onAddClick(selectedNetwork, selectedPortfolio).saveIn(addTokenJob) },
)

View file

@ -5,6 +5,7 @@ import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.addtoken.AddTokenUM
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
@ -35,13 +36,18 @@ internal class AddTokenUiBuilder @Inject constructor(
)
}
private fun createPortfolio(selectedPortfolio: SelectedPortfolio): PortfolioSelectUM {
private fun createPortfolio(
selectedPortfolio: SelectedPortfolio,
walletImage: UserWalletItemUM.ImageState,
): PortfolioSelectUM {
val accountIcon: AccountIconUM?
val portfolioName: TextReference
val imageState: UserWalletItemUM.ImageState?
when (selectedPortfolio.isAccountMode) {
false -> {
accountIcon = null
portfolioName = stringReference(selectedPortfolio.userWallet.name)
imageState = walletImage
}
true -> {
val accountStatus = selectedPortfolio.account.account
@ -50,6 +56,7 @@ internal class AddTokenUiBuilder @Inject constructor(
is CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon)
is Payment -> AccountIconUM.Payment
}
imageState = null
}
}
return PortfolioSelectUM(
@ -58,12 +65,14 @@ internal class AddTokenUiBuilder @Inject constructor(
isAccountMode = selectedPortfolio.isAccountMode,
isMultiChoice = selectedPortfolio.isAvailableMorePortfolio,
onClick = { params.callbacks.onChangePortfolioClick() },
imageState = imageState,
)
}
fun updateContent(
selectedPortfolio: SelectedPortfolio,
selectedNetwork: SelectedNetwork,
walletImage: UserWalletItemUM.ImageState,
isTangemIconVisible: Boolean,
onConfirmClick: () -> Unit,
): AddTokenUM {
@ -78,7 +87,7 @@ internal class AddTokenUiBuilder @Inject constructor(
onConfirmClick = onConfirmClick,
)
val networkUM = createNetwork(selectedNetwork)
val portfolioUM = createPortfolio(selectedPortfolio)
val portfolioUM = createPortfolio(selectedPortfolio, walletImage)
val currency = selectedNetwork.cryptoCurrency
val tokenToAdd = TokenItemState.Content(
id = currency.id.value,

View file

@ -8,7 +8,6 @@ import com.tangem.common.ui.tokens.TokenItemGrouping.toGroupedItems
import com.tangem.common.ui.tokens.TokenItemGrouping.toUngroupedItems
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.icons.IconTint
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
@ -55,12 +54,11 @@ internal class ChooseTokenListItemConverter(
}
private val fiatAmountStateProvider: ((TotalFiatBalance, isExpanded: Boolean) -> FiatAmountState?) =
{ totalBalance, isExpanded ->
when {
isSearchingState -> FiatAmountState.Empty
!isExpanded -> FiatAmountState.Icon(R.drawable.ic_chewron_down_20, IconTint.Informative)
else -> AccountCryptoPortfolioItemStateConverter
.createFiatAmountState(totalBalance, appCurrency)
{ totalBalance, _ ->
if (isSearchingState) {
FiatAmountState.Empty
} else {
AccountCryptoPortfolioItemStateConverter.createFiatAmountState(totalBalance, appCurrency)
}
}

View file

@ -1,5 +1,16 @@
package com.tangem.features.commonfeatures.impl.choosetoken.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.BoundsTransform
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -7,6 +18,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -15,16 +27,21 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.lerp
import androidx.compose.ui.text.style.TextAlign
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.common.ui.tokens.portfolioTokensList
import com.tangem.common.ui.tokens.NonContentItemContent
import com.tangem.common.ui.tokens.SlideInItemVisibility
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.account.toBoxSize
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.SearchBar
@ -34,6 +51,8 @@ import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.token.AccountItemPreviewData
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.NON_CONTENT_TOKENS_LIST_KEY
import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem
import com.tangem.core.ui.components.tokenlist.TokenListItem
import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
@ -41,26 +60,42 @@ import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.ds.image.DeviceIconUM
import com.tangem.core.ui.ds.image.TangemDeviceIcon
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.header.TangemHeaderRow
import com.tangem.core.ui.ds.row.internal.TangemRowTailUM
import com.tangem.core.ui.ds.row.token.TangemTokenRow
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.ds.row.token.internal.TokenRowTitle
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.BuyTokenScreenTestTags
import com.tangem.core.ui.utils.ProvideSharedTransitionScope
import com.tangem.core.ui.utils.TangemSharedTransitionLayout
import com.tangem.core.ui.utils.lazyListItemPosition
import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection
import com.tangem.core.ui.utils.sharedBoundsSafely
import com.tangem.features.commonfeatures.api.choosetoken.model.ChooseTokenPortfolioFullBlockUM
import com.tangem.features.commonfeatures.api.choosetoken.model.TokenListUMData
import com.tangem.features.commonfeatures.api.choosetoken.model.WalletListUM
import com.tangem.features.commonfeatures.api.choosetoken.model.WalletTabUM
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketState
import com.tangem.utils.StringsSigns.DOT
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlin.random.Random
private const val LOAD_MORE_BUFFER = 25
private const val ACCOUNT_COLLAPSE_STEP_MS = 50
private const val ACCOUNT_COLLAPSE_MAX_DELAY_MS = 250
private const val ACCOUNT_COLLAPSE_BASE_DELAY_MS = 150
private const val ACCOUNT_CONTENT_ANIM_MS = 350
private const val ACCOUNT_CONTENT_ANIM_DELAY_MS = 90
private const val ACCOUNT_BOUNDS_ANIM_MS = 250
private val ChooseTokenFullUM.isNotFoundState: Boolean
get() {
@ -298,11 +333,10 @@ private fun LazyListScope.tokensListItems(tokensListData: TokenListUMData, isBal
when (tokensListData) {
is TokenListUMData.AccountList -> {
tokensListData.tokensList.forEachIndexed { index, item ->
portfolioTokensList(
accountWithTokens(
portfolio = item,
portfolioIndex = index,
isBalanceHidden = isBalanceHidden,
testTag = BuyTokenScreenTestTags.LAZY_LIST_ITEM,
)
}
}
@ -338,6 +372,298 @@ private fun LazyListScope.tokensList(items: ImmutableList<TokensListItemUM>, isB
)
}
@Suppress("LongMethod")
private fun LazyListScope.accountWithTokens(
portfolio: TokensListItemUM.Portfolio,
portfolioIndex: Int,
isBalanceHidden: Boolean,
) {
val tokens = portfolio.tokens
val isExpanded = portfolio.isExpanded
val lastIndex = maxOf(tokens.lastIndex.inc(), 1)
item(key = "account-${portfolio.id}", contentType = "choose-token-account") {
val effectiveLastIndex by animateIntAsState(
targetValue = if (isExpanded) lastIndex else 0,
animationSpec = if (isExpanded) {
snap()
} else {
snap(
delayMillis = minOf(
ACCOUNT_COLLAPSE_STEP_MS * maxOf(tokens.lastIndex, 0),
ACCOUNT_COLLAPSE_MAX_DELAY_MS,
) + ACCOUNT_COLLAPSE_BASE_DELAY_MS,
)
},
label = "accountLastIndex",
)
AccountRow(
portfolio = portfolio,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
.semantics { lazyListItemPosition = portfolioIndex }
.roundedShapeItemDecoration(
currentIndex = 0,
radius = TangemTheme.dimens.radius14,
lastIndex = effectiveLastIndex,
backgroundColor = TangemTheme.colors.background.primary,
),
)
}
if (portfolio.content is PortfolioItemContentUM.Empty) {
item(key = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}") {
SlideInItemVisibility(
visible = isExpanded,
currentIndex = 1,
lastIndex = lastIndex,
modifier = Modifier.roundedShapeItemDecoration(
currentIndex = 1,
radius = TangemTheme.dimens.radius14,
lastIndex = 1,
backgroundColor = TangemTheme.colors.background.primary,
),
) {
NonContentItemContent(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16))
}
}
return
}
itemsIndexed(
items = tokens,
key = { _, token -> "${token.id}-choose-account-${portfolio.id}" },
contentType = { _, token -> token::class.java },
) { tokenIndex, token ->
val indexWithHeader = tokenIndex.inc()
val lastTokenBottomPadding = TangemTheme.dimens.spacing8
SlideInItemVisibility(
visible = isExpanded,
currentIndex = tokenIndex,
lastIndex = lastIndex,
modifier = Modifier
.testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
.roundedShapeItemDecoration(
currentIndex = indexWithHeader,
radius = TangemTheme.dimens.radius14,
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors.background.primary,
),
) {
PortfolioTokensListItem(
state = token,
isBalanceHidden = isBalanceHidden,
modifier = Modifier.conditional(indexWithHeader == lastIndex) {
padding(bottom = lastTokenBottomPadding)
},
)
}
}
}
@Suppress("LongMethod")
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun AccountRow(
portfolio: TokensListItemUM.Portfolio,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
val tokenRowUM = accountTokenRowUM(portfolio)
val subtitle = (tokenRowUM.subtitleUM as? TangemTokenRowUM.SubtitleUM.Content)?.text
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
ProvideSharedTransitionScope(Modifier.weight(1f)) {
val iconSharedContentState = rememberSharedContentState(key = "account-icon-${portfolio.id}")
val titleSharedContentState = rememberSharedContentState(key = "account-title-${portfolio.id}")
val boundsTransform = BoundsTransform { _, _ -> tween(ACCOUNT_BOUNDS_ANIM_MS) }
AnimatedContent(
targetState = portfolio.isExpanded,
transitionSpec = {
fadeIn(animationSpec = tween(ACCOUNT_CONTENT_ANIM_MS, delayMillis = ACCOUNT_CONTENT_ANIM_DELAY_MS))
.togetherWith(fadeOut(animationSpec = tween(ACCOUNT_CONTENT_ANIM_MS)))
},
label = "accountExpand",
) { isExpandedState ->
val animatedContentScope = this
val composables = remember(tokenRowUM) {
AccountRowComposables(
icon = { iconModifier ->
val iconSize = if (isExpandedState) {
AccountIconSize.RedesignExtraSmall
} else {
AccountIconSize.RedesignedDefault
}
val sizedIcon = when (val icon = tokenRowUM.headIconUM) {
is TangemIconUM.Currency -> icon.copy(
currencyIconState = when (val iconState = icon.currencyIconState) {
is CurrencyIconState.CryptoPortfolio.Icon -> iconState.copy(size = iconSize)
is CurrencyIconState.CryptoPortfolio.Letter -> iconState.copy(size = iconSize)
else -> iconState
},
)
else -> icon
}
TangemIcon(
tangemIconUM = sizedIcon,
modifier = iconModifier
.size(iconSize.toBoxSize())
.sharedBoundsSafely(
sharedContentState = iconSharedContentState,
animatedVisibilityScope = animatedContentScope,
boundsTransform = boundsTransform,
),
)
},
title = { titleModifier ->
val targetFraction = if (isExpandedState) 0f else 1f
val animationFraction = animateFloatAsState(
targetValue = targetFraction,
animationSpec = tween(durationMillis = ACCOUNT_CONTENT_ANIM_MS),
label = "accountTitle",
)
val startStyle = TangemTheme.typography2.captionSemibold12
val stopStyle = TangemTheme.typography2.bodySemibold16
val textStyle by remember(animationFraction.value) {
derivedStateOf { lerp(startStyle, stopStyle, animationFraction.value) }
}
val resizedTitle = when (val titleUM = tokenRowUM.titleUM) {
is TangemTokenRowUM.TitleUM.Content -> titleUM.copy(
text = styledStringReference(
titleUM.text.resolveReference(),
{ textStyle.toSpanStyle() },
),
)
else -> titleUM
}
TokenRowTitle(
titleUM = resizedTitle,
modifier = titleModifier.sharedBoundsSafely(
sharedContentState = titleSharedContentState,
animatedVisibilityScope = animatedContentScope,
boundsTransform = boundsTransform,
resizeMode = scaleToBounds(ContentScale.Fit, Alignment.CenterStart),
),
)
},
)
}
if (isExpandedState) {
TangemHeaderRow(
subtitle = subtitle,
isBalanceHidden = isBalanceHidden,
titleContent = composables.title,
headContent = composables.icon,
tailUM = TangemRowTailUM.Empty,
onItemClick = tokenRowUM.onItemClick,
)
} else {
TangemTokenRow(
tokenRowUM = tokenRowUM,
isBalanceHidden = isBalanceHidden,
headComponent = composables.icon,
titleComponent = composables.title,
)
}
}
}
AccountTail(
isExpanded = portfolio.isExpanded,
onClick = { tokenRowUM.onItemClick?.invoke() },
)
}
}
@Composable
private fun AccountTail(isExpanded: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.padding(start = TangemTheme.dimens2.x2, end = TangemTheme.dimens2.x4)
.size(TangemTheme.dimens2.x9)
.clip(CircleShape)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center,
) {
AnimatedContent(
targetState = isExpanded,
contentAlignment = Alignment.Center,
label = "accountTail",
) { expanded ->
if (expanded) {
Icon(
modifier = Modifier
.offset(x = TangemTheme.dimens.spacing2)
.size(TangemTheme.dimens2.x4),
painter = painterResource(id = R.drawable.ic_minimize_24),
tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant,
contentDescription = null,
)
} else {
AccountExpandButton()
}
}
}
}
@Composable
private fun AccountExpandButton(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens2.x9)
.clip(CircleShape)
.background(TangemTheme.colors2.button.backgroundSecondary),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x5),
painter = painterResource(id = R.drawable.ic_chewron_down_20),
tint = TangemTheme.colors2.button.iconPrimary,
contentDescription = null,
)
}
}
@Stable
private class AccountRowComposables(
val title: @Composable (Modifier) -> Unit,
val icon: @Composable (Modifier) -> Unit,
)
private fun accountTokenRowUM(portfolio: TokensListItemUM.Portfolio): TangemTokenRowUM.Content {
val account = portfolio.tokenItemUM
val name = (account.titleState as? TokenItemState.TitleState.Content)?.text ?: TextReference.EMPTY
val tokensCount = (account.subtitleState as? TokenItemState.SubtitleState.TextContent)?.value
val balance = (account.fiatAmountState as? TokenItemState.FiatAmountState.Content)?.text
return TangemTokenRowUM.Content(
id = portfolio.id,
headIconUM = TangemIconUM.Currency(currencyIconState = account.iconState),
titleUM = TangemTokenRowUM.TitleUM.Content(text = name),
subtitleUM = buildAccountSubtitle(tokensCount, balance)
?.let { TangemTokenRowUM.SubtitleUM.Content(text = it) }
?: TangemTokenRowUM.SubtitleUM.Empty,
topEndContentUM = TangemTokenRowUM.EndContentUM.Empty,
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Empty,
tailUM = TangemRowTailUM.Empty,
onItemClick = { account.onItemClick?.invoke(account) },
onItemLongClick = null,
)
}
private fun buildAccountSubtitle(tokensCount: TextReference?, balance: String?): TextReference? {
return when {
tokensCount != null && balance != null ->
combinedReference(tokensCount, stringReference(" $DOT "), stringReference(balance))
tokensCount != null -> tokensCount
balance != null -> stringReference(balance)
else -> null
}
}
private fun LazyListScope.emptyTokensList(modifier: Modifier = Modifier) {
item("EmptyTokensList") {
Box(

View file

@ -30,7 +30,6 @@ import com.tangem.common.ui.markets.action.TokenActionsContext
import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.userportfolio.UserPortfolioComponent
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -42,15 +41,12 @@ internal class DefaultManageFundsComponent @AssistedInject constructor(
chooseTokenComponentFactory: ChooseTokenComponent.Factory,
tokenActionsComponentFactory: TokenActionsComponent.Factory,
userPortfolioComponentFactory: UserPortfolioComponent.Factory,
walletFeatureToggles: WalletFeatureToggles,
) : AppComponentContext by appComponentContext, ManageFundsComponent {
private val model: ManageFundsModel = getOrCreateModel(params)
private val isCompactTokenActions: Boolean = params.launchMode is ManageFundsComponent.LaunchMode.TokenActionsOnly
private val isAddFundsStage1Enabled: Boolean = walletFeatureToggles.isAddFundsStage1Enabled
private val tokenActionsComponent: TokenActionsComponent by lazy {
tokenActionsComponentFactory.create(
context = child(key = "manageFundsTokenActions"),
@ -107,7 +103,7 @@ internal class DefaultManageFundsComponent @AssistedInject constructor(
)
}
WithOptionalRedesignTheme(isEnabled = isAddFundsStage1Enabled) {
TangemThemeRedesign {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
onBack = if (canGoBack) model::onBack else ::dismiss,
config = TangemBottomSheetConfig(
@ -179,7 +175,7 @@ internal class DefaultManageFundsComponent @AssistedInject constructor(
) {
userPortfolioComponent.Content(modifier)
}
ManageFundsModel.UiRoute.TokenActions -> tokenActionsComponent.Content(modifier)
is ManageFundsModel.UiRoute.TokenActions -> tokenActionsComponent.Content(modifier)
}
}
@ -191,8 +187,13 @@ internal class DefaultManageFundsComponent @AssistedInject constructor(
onCloseClick: () -> Unit,
) {
val spec = route.uiSpec(model.flowType)
val title = if (route is ManageFundsModel.UiRoute.TokenActions) {
route.title
} else {
spec.title
}
TangemTopBar(
title = spec.title,
title = title,
subtitle = spec.subtitle,
type = TangemTopBarType.BottomSheet,
startContent = if (canGoBack) {
@ -219,15 +220,6 @@ internal class DefaultManageFundsComponent @AssistedInject constructor(
)
}
@Composable
private fun WithOptionalRedesignTheme(isEnabled: Boolean, content: @Composable () -> Unit) {
if (isEnabled) {
TangemThemeRedesign(content = content)
} else {
content()
}
}
@AssistedFactory
interface Factory : ManageFundsComponent.Factory {
override fun create(

View file

@ -10,6 +10,9 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
@ -26,6 +29,7 @@ import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenAnalyticsPa
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
import com.tangem.features.commonfeatures.api.tokenactions.BottomAction
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.managefunds.analytics.ManageFundsAnalyticsEvent
import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.userportfolio.state.UserPortfolioStateController
@ -206,7 +210,7 @@ internal class ManageFundsModel @Inject constructor(
return@launch
}
tokenActionsTrigger.value = TokenActionsRequest(wallet, match.first, match.second)
replaceRoot(UiRoute.TokenActions)
replaceRoot(tokenActionsRoute(match.second))
}
}
@ -218,7 +222,7 @@ internal class ManageFundsModel @Inject constructor(
1 -> {
val entry = entries.first()
tokenActionsTrigger.value = TokenActionsRequest(entry.userWallet, entry.account, entry.status)
replaceRoot(UiRoute.TokenActions)
replaceRoot(tokenActionsRoute(entry.status))
}
else -> {
filteredEntries.value = entries
@ -261,7 +265,12 @@ internal class ManageFundsModel @Inject constructor(
private fun openTokenActions(request: TokenActionsRequest, bottomAction: BottomAction) {
tokenActionsTrigger.value = request
currentBottomAction.value = bottomAction
pushRoute(UiRoute.TokenActions)
pushRoute(tokenActionsRoute(request.status))
}
private fun tokenActionsRoute(status: CryptoCurrencyStatus): UiRoute.TokenActions {
val title = resourceReference(R.string.get_token_title, wrappedList(status.currency.name))
return UiRoute.TokenActions(title = title)
}
private fun replaceRoot(route: UiRoute) {
@ -277,7 +286,7 @@ internal class ManageFundsModel @Inject constructor(
data object Loading : UiRoute
data object ChooseToken : UiRoute
data object UserPortfolio : UiRoute
data object TokenActions : UiRoute
data class TokenActions(val title: TextReference) : UiRoute
}
private data class TokenActionsRequest(

View file

@ -33,7 +33,7 @@ internal fun ManageFundsModel.UiRoute.uiSpec(flowType: ManageFundsComponent.Flow
shouldApplyHorizontalPadding = false,
shouldFillHeight = false,
)
ManageFundsModel.UiRoute.TokenActions -> ManageFundsRouteUiSpec(
is ManageFundsModel.UiRoute.TokenActions -> ManageFundsRouteUiSpec(
title = resourceReference(if (isTransfer) R.string.common_transfer else R.string.common_get_token),
subtitle = null,
shouldApplyHorizontalPadding = true,

View file

@ -31,6 +31,7 @@ import com.tangem.features.commonfeatures.impl.R
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.wallets.usecase.GetWalletIconUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.tokenactions.ui.state.PortfolioBadgeUM
import com.tangem.features.commonfeatures.impl.tokenactions.ui.state.TokenActionsUM
@ -42,6 +43,7 @@ internal class TokenActionsUiBuilder @Inject constructor(
paramsContainer: ParamsContainer,
private val designFeatureToggles: DesignFeatureToggles,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val walletIconUMConverter: WalletIconUMConverter,
) {
private val params = paramsContainer.require<TokenActionsComponent.Params>()
@ -145,38 +147,47 @@ internal class TokenActionsUiBuilder @Inject constructor(
}
private fun createPortfolioBadge(cryptoCurrencyData: CryptoCurrencyData): PortfolioBadgeUM {
return if (cryptoCurrencyData.isAccountMode) {
val icon = CryptoPortfolioIconConverter.convert(cryptoCurrencyData.account.account.icon)
val name = cryptoCurrencyData
.account
.account
.accountName
.toUM()
.value
PortfolioBadgeUM.Account(
badge = TangemBadgeUM(
text = name,
tangemIconUM = TangemIconUM.Icon(
iconRes = icon.value.getResId(),
tintReference = { icon.color.getUiColor() },
return when {
cryptoCurrencyData.isAccountMode -> {
val icon = CryptoPortfolioIconConverter.convert(cryptoCurrencyData.account.account.icon)
val name = cryptoCurrencyData
.account
.account
.accountName
.toUM()
.value
PortfolioBadgeUM.Account(
badge = TangemBadgeUM(
text = name,
tangemIconUM = TangemIconUM.Icon(
iconRes = icon.value.getResId(),
tintReference = { icon.color.getUiColor() },
),
size = TangemBadgeSize.X6,
shape = TangemBadgeShape.Rounded,
iconPosition = TangemBadgeIconPosition.Start,
shouldRespectIconTint = true,
),
size = TangemBadgeSize.X6,
shape = TangemBadgeShape.Rounded,
iconPosition = TangemBadgeIconPosition.Start,
shouldRespectIconTint = true,
),
)
} else {
val userWallet = cryptoCurrencyData.userWallet
PortfolioBadgeUM.Wallet(
name = stringReference(userWallet.name),
deviceIcon = walletIconUMConverter.convert(
getWalletIconUseCase(cryptoCurrencyData.userWallet),
),
)
)
}
isSingleWallet() -> PortfolioBadgeUM.None
else -> {
val userWallet = cryptoCurrencyData.userWallet
PortfolioBadgeUM.Wallet(
name = stringReference(userWallet.name),
deviceIcon = walletIconUMConverter.convert(
getWalletIconUseCase(cryptoCurrencyData.userWallet),
),
)
}
}
}
private fun isSingleWallet(): Boolean {
val count = runCatching { getWalletsUseCase.invokeSync().size }.getOrNull() ?: return false
return count <= 1
}
private fun createSubtitle2State(status: CryptoCurrencyStatus): TokenItemState.Subtitle2State? {
return when (status.value) {
is CryptoCurrencyStatus.Loaded,