From 5b031d5c0ef7b04803559fa7cc688f567cfa96ef Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 13:24:17 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../common/ui/account/PortfolioSelectRow.kt | 15 + core/res/src/main/res/values/strings.xml | 1 + .../AddToPortfolioBottomSheet.kt | 8 +- .../DefaultAddToPortfolioComponent.kt | 2 +- .../model/AddToPortfolioModel.kt | 4 +- .../model/AddToPortfolioRouteUiSpec.kt | 5 +- .../model/AddToPortfolioRoutes.kt | 2 +- .../addtoportfolio/model/AddTokenModel.kt | 14 +- .../addtoportfolio/model/AddTokenUiBuilder.kt | 13 +- .../converter/ChooseTokenListItemConverter.kt | 12 +- .../impl/choosetoken/ui/ChooseTokenScreen.kt | 332 +++++++++++++++++- .../DefaultManageFundsComponent.kt | 24 +- .../managefunds/model/ManageFundsModel.kt | 17 +- .../model/ManageFundsRouteUiSpec.kt | 2 +- .../model/TokenActionsUiBuilder.kt | 67 ++-- 15 files changed, 448 insertions(+), 70 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt b/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt index 9dd905225f..2e6d8cc553 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt @@ -20,6 +20,8 @@ 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.R +import com.tangem.common.ui.userwallet.CardImage +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.SpacerW12 import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.ds.row.TangemRowContainer @@ -111,6 +113,18 @@ fun PortfolioSelectRowV2( size = AccountIconSize.RedesignedDefault, ) } + } else if (state.imageState != null) { + Box( + modifier = Modifier + .layoutId(TangemRowLayoutId.HEAD) + .padding(end = TangemTheme.dimens2.x3), + contentAlignment = Alignment.Center, + ) { + CardImage( + modifier = Modifier.size(TangemTheme.dimens2.x10), + imageState = state.imageState, + ) + } } Row( @@ -156,6 +170,7 @@ data class PortfolioSelectUM( val isAccountMode: Boolean, val isMultiChoice: Boolean, val onClick: () -> Unit, + val imageState: UserWalletItemUM.ImageState? = null, ) @Preview(widthDp = 360, showBackground = true) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b72b7efbc3..a2da937bc0 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -727,6 +727,7 @@ An error occurred An error occurred. Code: %s. Requires memo + Get %1$s Transaction By approving, you allow the smart contract to use your tokens in future transactions. Amount %s diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt index 3f2288d1f4..e1d4f00182 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt @@ -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 diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt index 6c9970ee38..d3652b2d02 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt @@ -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( diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt index 1cc4c726fb..5369f159bc 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt @@ -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) diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRouteUiSpec.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRouteUiSpec.kt index 68d861ed3a..4a2311cea0 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRouteUiSpec.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRouteUiSpec.kt @@ -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, diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt index 8c01391c47..80e3cf7632 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt @@ -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 } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenModel.kt index 7b7facbc0e..871aa665ed 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenModel.kt @@ -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) }, ) diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenUiBuilder.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenUiBuilder.kt index fc6d0e2afd..662e9f6754 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenUiBuilder.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenUiBuilder.kt @@ -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, diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/converter/ChooseTokenListItemConverter.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/converter/ChooseTokenListItemConverter.kt index 3ca744cb50..84238b8387 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/converter/ChooseTokenListItemConverter.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/converter/ChooseTokenListItemConverter.kt @@ -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) } } diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt index a9af5504aa..65166caad5 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt @@ -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, 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( diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/DefaultManageFundsComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/DefaultManageFundsComponent.kt index 8354a085f3..b2ae5d9dda 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/DefaultManageFundsComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/DefaultManageFundsComponent.kt @@ -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( 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( diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt index 71f307963d..a1178676e5 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt @@ -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( diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsRouteUiSpec.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsRouteUiSpec.kt index 7fd98aecd9..a9911c0b98 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsRouteUiSpec.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsRouteUiSpec.kt @@ -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, diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/model/TokenActionsUiBuilder.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/model/TokenActionsUiBuilder.kt index cc1ec83458..e6ad94e41a 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/model/TokenActionsUiBuilder.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/model/TokenActionsUiBuilder.kt @@ -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() @@ -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,