From 70dd41a6803b976319ccef8a6cd81438e48af2da Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 5 Nov 2025 03:49:15 +0700 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 8 + domain/nft/build.gradle.kts | 1 + .../domain/nft/models/WalletNFTCollections.kt | 9 + .../domain/nft/GetNFTCollectionsUseCase.kt | 49 ++- .../domain/nft/GetNFTNetworksUseCase.kt | 50 ++- .../fetcher/DefaultPortfolioFetcher.kt | 5 +- .../selector/PortfolioSelectorModel.kt | 16 +- features/nft/impl/build.gradle.kts | 3 + .../nft/collections/entity/NFTCollectionUM.kt | 14 +- .../collections/entity/NFTCollectionsUM.kt | 2 +- ...hangeCollectionExpandedStateTransformer.kt | 3 +- .../transformer/UpdateDataStateTransformer.kt | 49 ++- .../collections/model/NFTCollectionsModel.kt | 42 +- .../collections/ui/NFTCollectionsContent.kt | 369 ++++++++++++------ .../nft/common/DefaultNFTComponent.kt | 90 ++++- .../tangem/features/nft/common/NFTRoute.kt | 3 +- .../nft/receive/NFTReceiveComponent.kt | 5 +- .../nft/receive/model/NFTReceiveModel.kt | 58 ++- .../account/ExpandedAccountsHolder.kt | 2 +- 19 files changed, 594 insertions(+), 184 deletions(-) create mode 100644 domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/WalletNFTCollections.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index d16fc22bd8..ab440ee279 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -1,5 +1,7 @@ package com.tangem.tap.di.domain +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.nft.* import com.tangem.domain.nft.repository.NFTRepository @@ -24,9 +26,13 @@ internal object NFTDomainModule { fun providesGetNFTCollectionsUseCase( currenciesRepository: CurrenciesRepository, nftRepository: NFTRepository, + singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + accountsFeatureToggles: AccountsFeatureToggles, ): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase( currenciesRepository = currenciesRepository, nftRepository = nftRepository, + singleAccountStatusListSupplier = singleAccountStatusListSupplier, + accountsFeatureToggles = accountsFeatureToggles, ) @Provides @@ -60,10 +66,12 @@ internal object NFTDomainModule { @Singleton fun providesGetNFTAvailableNetworksUseCase( nftRepository: NFTRepository, + singleAccountStatusListSupplier: SingleAccountStatusListSupplier, currenciesRepository: CurrenciesRepository, ): GetNFTNetworksUseCase = GetNFTNetworksUseCase( currenciesRepository = currenciesRepository, nftRepository = nftRepository, + singleAccountStatusListSupplier = singleAccountStatusListSupplier, ) @Provides diff --git a/domain/nft/build.gradle.kts b/domain/nft/build.gradle.kts index 6752c4736d..e7b3973752 100644 --- a/domain/nft/build.gradle.kts +++ b/domain/nft/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { // region Project – Domain implementation(projects.domain.core) + implementation(projects.domain.account.status) implementation(projects.domain.models) implementation(projects.domain.networks) implementation(projects.domain.nft.models) diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/WalletNFTCollections.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/WalletNFTCollections.kt new file mode 100644 index 0000000000..f6526e3509 --- /dev/null +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/WalletNFTCollections.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.nft.models + +import com.tangem.domain.models.account.Account + +data class WalletNFTCollections( + val collections: Map>, +) { + val flattenCollections by lazy { collections.values.flatten() } +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt index 954646a953..b79f69823d 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt @@ -1,25 +1,54 @@ package com.tangem.domain.nft +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.models.NFTCollections +import com.tangem.domain.nft.models.WalletNFTCollections import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.* class GetNFTCollectionsUseCase( private val currenciesRepository: CurrenciesRepository, private val nftRepository: NFTRepository, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val accountsFeatureToggles: AccountsFeatureToggles, ) { @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWalletId: UserWalletId): Flow> = currenciesRepository - .getWalletCurrenciesUpdates(userWalletId) - .flatMapLatest { - val networks = it - .map { cryptoCurrency -> cryptoCurrency.network } - .distinct() - nftRepository.observeCollections(userWalletId, networks) + operator fun invoke(userWalletId: UserWalletId): Flow> = + if (accountsFeatureToggles.isFeatureEnabled) { + invokeForAccounts(userWalletId).map { it.flattenCollections } + } else { + currenciesRepository + .getWalletCurrenciesUpdates(userWalletId) + .flatMapLatest { + nftCollections(userWalletId, it) + } } + + fun invokeForAccounts(userWalletId: UserWalletId): Flow { + fun AccountStatus.flowOfNFTCollections(): Flow>> = + nftCollections(userWalletId, this.flattenCurrencies().map { it.currency }) + .map { nfts -> this.account to nfts } + + return singleAccountStatusListSupplier(userWalletId) + .mapLatest { statusList -> statusList.accountStatuses.map { it.flowOfNFTCollections() } } + .flatMapLatest { flows -> combine(flows) { WalletNFTCollections(it.toMap()) } } + } + + private fun nftCollections( + userWalletId: UserWalletId, + cryptoCurrencies: List, + ): Flow> { + val networks = cryptoCurrencies + .map { cryptoCurrency -> cryptoCurrency.network } + .distinct() + return nftRepository.observeCollections(userWalletId, networks) + } } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt index 74a6eea33e..e3f8d2c71e 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt @@ -1,32 +1,48 @@ package com.tangem.domain.nft +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.models.PortfolioId +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.models.NFTNetworks import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.mapNotNull class GetNFTNetworksUseCase( private val currenciesRepository: CurrenciesRepository, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val nftRepository: NFTRepository, ) { - operator fun invoke(userWalletId: UserWalletId): Flow = currenciesRepository - .getWalletCurrenciesUpdates(userWalletId) - .map { cryptoCurrencies -> - val availableNetworks = cryptoCurrencies - .map { cryptoCurrency -> cryptoCurrency.network } - .filter { nftRepository.isNFTSupported(userWalletId, it) } - .sortedBy { it.name } + operator fun invoke(portfolioId: PortfolioId): Flow = when (portfolioId) { + is PortfolioId.Account -> singleAccountStatusListSupplier(portfolioId.userWalletId) + .map { it.accountStatuses } + .mapNotNull { accountStatuses -> accountStatuses.find { it.account.accountId == portfolioId.accountId } } + .map { accountStatus -> accountStatus.flattenCurrencies().map { it.currency } } + .mapLatest { it.toNFTNetworks(portfolioId.userWalletId) } + is PortfolioId.Wallet -> + currenciesRepository + .getWalletCurrenciesUpdates(portfolioId.userWalletId) + .map { cryptoCurrencies -> cryptoCurrencies.toNFTNetworks(portfolioId.userWalletId) } + } - val unavailableNetworks = nftRepository - .getNFTSupportedNetworks(userWalletId) - .filter { supportedNetwork -> availableNetworks.none { it.id == supportedNetwork.id } } - .sortedBy { it.name } + private suspend fun List.toNFTNetworks(userWalletId: UserWalletId): NFTNetworks { + val availableNetworks = this + .map { cryptoCurrency -> cryptoCurrency.network } + .filter { nftRepository.isNFTSupported(userWalletId, it) } + .sortedBy { it.name } - NFTNetworks( - availableNetworks = availableNetworks, - unavailableNetworks = unavailableNetworks, - ) - } + val unavailableNetworks = nftRepository + .getNFTSupportedNetworks(userWalletId) + .filter { supportedNetwork -> availableNetworks.none { it.id == supportedNetwork.id } } + .sortedBy { it.name } + + return NFTNetworks( + availableNetworks = availableNetworks, + unavailableNetworks = unavailableNetworks, + ) + } } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt b/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt index 010ebe39f3..b8d6318fe9 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt @@ -41,7 +41,10 @@ internal class DefaultPortfolioFetcher @AssistedInject constructor( get() = _mode init { - _mode.flatMapLatest(::combineUseCases) + _mode + // reset cache if mode(StateFlow) changed + .onEach { _data.resetReplayCache() } + .flatMapLatest(::combineUseCases) .flowOn(dispatchers.default) .onEach { _data.emit(it) } .launchIn(scope) diff --git a/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt index 86d7e31fc3..fa399e099b 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt @@ -147,11 +147,17 @@ internal class PortfolioSelectorModel @Inject constructor( return@forEach } - val walletTitle = PortfolioSelectorItemUM.GroupTitle( - id = "GroupTitle ${wallet.walletId.stringValue}", - name = stringReference(wallet.name), - ) - add(walletTitle) + when (balanceFetcher.mode.value) { + // for Wallet mode expected single portfolioData.balances + is PortfolioFetcher.Mode.Wallet -> Unit + is PortfolioFetcher.Mode.All -> { + val walletTitle = PortfolioSelectorItemUM.GroupTitle( + id = "GroupTitle ${wallet.walletId.stringValue}", + name = stringReference(wallet.name), + ) + add(walletTitle) + } + } portfolio.accountsBalance.accountStatuses.forEach { accountStatus -> val isEnabledByFeature = isEnabled(wallet, accountStatus) diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index 438a815cc3..693be634b3 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -13,6 +13,7 @@ android { dependencies { /** Api */ + implementation(projects.features.account.api) implementation(projects.features.nft.api) implementation(projects.features.tokenRecieve.api) @@ -28,6 +29,8 @@ dependencies { implementation(projects.core.datasource) /** Domain modules */ + implementation(projects.domain.account) + implementation(projects.domain.wallets) implementation(projects.domain.appCurrency.models) implementation(projects.domain.appCurrency) implementation(projects.domain.models) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt index e7e24ded3b..fd859b1c83 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt @@ -1,10 +1,20 @@ package com.tangem.features.nft.collections.entity import androidx.annotation.DrawableRes +import com.tangem.common.ui.account.AccountTitleUM import com.tangem.core.ui.extensions.TextReference +internal sealed interface NFTCollectionItem { + val id: String +} + +internal data class NFTCollectionPortfolioUM( + override val id: String, + val title: AccountTitleUM, +) : NFTCollectionItem + internal data class NFTCollectionUM( - val id: String, + override val id: String, val name: String, @DrawableRes val networkIconId: Int, val logoUrl: String?, @@ -12,4 +22,4 @@ internal data class NFTCollectionUM( val assets: NFTCollectionAssetsListUM, val isExpanded: Boolean, val onExpandClick: () -> Unit, -) \ No newline at end of file +) : NFTCollectionItem \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt index a8287ca423..9388efcd65 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt @@ -23,7 +23,7 @@ internal sealed class NFTCollectionsUM { data class Content( val search: SearchBarUM, - val collections: ImmutableList, + val collections: ImmutableList, val warnings: ImmutableList, val onReceiveClick: () -> Unit, ) : NFTCollectionsUM() diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeCollectionExpandedStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeCollectionExpandedStateTransformer.kt index 56ebea5c6b..13ec03bca2 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeCollectionExpandedStateTransformer.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeCollectionExpandedStateTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.features.nft.collections.entity.transformer import com.tangem.domain.nft.models.NFTCollection +import com.tangem.features.nft.collections.entity.NFTCollectionUM import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM import com.tangem.features.nft.collections.entity.NFTCollectionsUM import com.tangem.utils.transformer.Transformer @@ -21,7 +22,7 @@ internal class ChangeCollectionExpandedStateTransformer( is NFTCollectionsUM.Content -> prevState.content.copy( collections = prevState.content.collections.map { val collectionId = collection.collectionIdProvider() - if (it.id == collectionId) { + if (it.id == collectionId && it is NFTCollectionUM) { if (!it.isExpanded) { onFirstExpanded() } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt index da0d617e10..431de33406 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt @@ -1,10 +1,13 @@ package com.tangem.features.nft.collections.entity.transformer +import com.tangem.common.ui.account.AccountTitleUM +import com.tangem.common.ui.account.toUM import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.account.Account import com.tangem.domain.nft.models.* import com.tangem.features.nft.collections.entity.* import com.tangem.features.nft.impl.R @@ -16,6 +19,8 @@ import kotlinx.collections.immutable.toPersistentList @Suppress("LongParameterList") internal class UpdateDataStateTransformer( private val nftCollections: List, + private val walletNFTCollections: WalletNFTCollections? = null, + private val isAccountMode: Boolean = false, private val onReceiveClick: () -> Unit, private val onRetryClick: () -> Unit, private val onExpandCollectionClick: (NFTCollection) -> Unit, @@ -25,7 +30,9 @@ internal class UpdateDataStateTransformer( private val collectionIdProvider: NFTCollection.() -> String, ) : Transformer { + @Suppress("CyclomaticComplexMethod") override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM { + val nftCollections = walletNFTCollections?.flattenCollections ?: this.nftCollections val hasQuery = !(prevState.content as? NFTCollectionsUM.Content)?.search?.query.isNullOrEmpty() val content = when { !hasQuery && nftCollections.allCollectionsFailed() -> @@ -66,16 +73,45 @@ internal class UpdateDataStateTransformer( } else { initialSearchBarFactory() }, - collections = nftCollections + collections = walletNFTCollections + ?.let { createCollections(it) } + ?: createNFTsUM(nftCollections).toPersistentList(), + warnings = transformNotifications(), + onReceiveClick = onReceiveClick, + ) + + private fun NFTCollectionsStateUM.createCollections(walletNFTCollections: WalletNFTCollections) = + if (isAccountMode) { + val result = mutableListOf() + walletNFTCollections.collections.forEach { (account, nfts) -> + if (nfts.isEmpty()) return@forEach + result.add(account.toAccountPortfolioUM()) + result.addAll(createNFTsUM(nfts)) + } + result.toPersistentList() + } else { + val mainAccountCollection = walletNFTCollections.collections.values.firstOrNull() ?: listOf() + createNFTsUM(mainAccountCollection).toPersistentList() + } + + private fun Account.toAccountPortfolioUM(): NFTCollectionPortfolioUM = NFTCollectionPortfolioUM( + id = this.accountId.value, + title = AccountTitleUM.Account( + prefixText = TextReference.EMPTY, + name = this.accountName.toUM().value, + icon = when (this) { + is Account.CryptoPortfolio -> this.icon.toUM() + }, + ), + ) + + private fun NFTCollectionsStateUM.createNFTsUM(nftCollections: List): Sequence = + nftCollections .map { it.content } .asSequence() .filterIsInstance() .map { it.collections.orEmpty().transform(this) } .flatten() - .toPersistentList(), - warnings = transformNotifications(), - onReceiveClick = onReceiveClick, - ) private fun List.transform(state: NFTCollectionsStateUM): ImmutableList = map { NFTCollectionUM( @@ -97,6 +133,8 @@ internal class UpdateDataStateTransformer( }.toPersistentList() private fun transformNotifications(): ImmutableList = buildList { + val nftCollections = walletNFTCollections?.flattenCollections + ?: this@UpdateDataStateTransformer.nftCollections if (nftCollections.anyCollectionFailed()) { add( NFTCollectionsWarningUM( @@ -152,6 +190,7 @@ internal class UpdateDataStateTransformer( private fun NFTCollection.isExpanded(state: NFTCollectionsStateUM): Boolean = (state.content as? NFTCollectionsUM.Content) ?.collections + ?.filterIsInstance() ?.firstOrNull { it.id == this.collectionIdProvider() } ?.isExpanded ?: false diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt index 5a5fe32b0f..20f1fc3414 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt @@ -7,6 +7,8 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi import com.tangem.core.ui.components.fields.InputManager import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.nft.RefreshAllNFTUseCase @@ -31,6 +33,8 @@ internal class NFTCollectionsModel @Inject constructor( private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val fetchNFTCollectionAssetsUseCase: FetchNFTCollectionAssetsUseCase, private val refreshAllNFTUseCase: RefreshAllNFTUseCase, + private val accountsFeatureToggles: AccountsFeatureToggles, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -62,7 +66,11 @@ internal class NFTCollectionsModel @Inject constructor( } init { - subscribeToNFTCollections() + if (accountsFeatureToggles.isFeatureEnabled) { + subscribeToNFTCollectionsNew() + } else { + subscribeToNFTCollections() + } } private fun subscribeToNFTCollections() { @@ -91,6 +99,38 @@ internal class NFTCollectionsModel @Inject constructor( .launchIn(modelScope) } + private fun subscribeToNFTCollectionsNew() { + combine( + flow = getNFTCollectionsUseCase.invokeForAccounts(params.userWalletId), + flow2 = searchManager.query.distinctUntilChanged(), + flow3 = isAccountsModeEnabledUseCase(), + ) { nftCollections, query, isAccountMode -> + val filteredNFTs = nftCollections.collections + .mapValues { (_, nfts) -> nfts.filter(query) } + + _state.update { + UpdateDataStateTransformer( + nftCollections = listOf(), + isAccountMode = isAccountMode, + walletNFTCollections = nftCollections.copy(collections = filteredNFTs), + onReceiveClick = { + params.onReceiveClick() + }, + onRetryClick = ::onRefresh, + onExpandCollectionClick = ::onExpandCollectionClick, + onRetryAssetsClick = ::onRetryAssetsClick, + onAssetClick = { asset, collection -> + params.onAssetClick(asset, collection) + }, + initialSearchBarFactory = ::getInitialSearchBar, + collectionIdProvider = collectionIdProvider, + ).transform(it) + } + } + .onStart { onRefresh() } + .launchIn(modelScope) + } + private fun List.filter(query: String): List = map { it.copy( content = when (val content = it.content) { diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt index e53a86b841..a134698b02 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt @@ -8,30 +8,38 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Shape 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 androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.common.ui.account.AccountIconPreviewData +import com.tangem.common.ui.account.AccountTitle +import com.tangem.common.ui.account.AccountTitleUM import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.TangemSearchBarDefaults import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.nft.collections.entity.* import com.tangem.features.nft.impl.R import kotlinx.collections.immutable.persistentListOf +import java.util.UUID @Suppress("LongMethod") @Composable @@ -89,41 +97,24 @@ internal fun NFTCollectionsContent(content: NFTCollectionsUM.Content, modifier: .padding( top = TangemTheme.dimens.spacing16, bottom = bottomPadding, - ) - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.primary), + ), + // .clip(TangemTheme.shapes.roundedCornersXMedium) + // .background(TangemTheme.colors.background.primary), state = listState, ) { - content.collections.fastForEach { collection -> - item(key = collection.id) { - NFTCollection( - modifier = Modifier.fillMaxWidth(), - state = collection, + content.collections.fastForEachIndexed { index, item -> + val previousItem = content.collections.getOrNull(index.dec()) + val nextItem = content.collections.getOrNull(index.inc()) + when (item) { + is NFTCollectionPortfolioUM -> nftPortfolioItem( + item = item, + shape = getRoundShape(item, previousItem, nextItem), + ) + is NFTCollectionUM -> nftCollectionItem( + collection = item, + shape = getRoundShape(item, previousItem, nextItem), + isNextItemNFTCollection = nextItem is NFTCollectionUM, ) - } - when (val assets = collection.assets) { - is NFTCollectionAssetsListUM.Init -> Unit - is NFTCollectionAssetsListUM.Loading -> { - assetsListLoading( - collectionId = collection.id, - content = assets, - expanded = collection.isExpanded, - ) - } - is NFTCollectionAssetsListUM.Failed -> { - assetsListFailed( - collectionId = collection.id, - content = assets, - expanded = collection.isExpanded, - ) - } - is NFTCollectionAssetsListUM.Content -> { - assetsListContent( - collectionId = collection.id, - content = assets, - expanded = collection.isExpanded, - ) - } } } } @@ -139,9 +130,94 @@ internal fun NFTCollectionsContent(content: NFTCollectionsUM.Content, modifier: } } +private fun getRoundShape( + item: NFTCollectionItem, + previousItem: NFTCollectionItem?, + nextItem: NFTCollectionItem?, +): RoundedCornerShape { + val radius = 16.dp + val topRound = RoundedCornerShape(topStart = radius, topEnd = radius) + val bottomRound = RoundedCornerShape(bottomStart = radius, bottomEnd = radius) + val allRound = RoundedCornerShape(size = radius) + return when (item) { + is NFTCollectionPortfolioUM -> topRound + is NFTCollectionUM -> when { + previousItem == null && nextItem == null -> allRound + previousItem == null && nextItem is NFTCollectionUM -> topRound + previousItem != null && nextItem !is NFTCollectionUM -> bottomRound + else -> RoundedCornerShape(0.dp) + } + } +} + +private fun LazyListScope.nftPortfolioItem(item: NFTCollectionPortfolioUM, shape: RoundedCornerShape) { + item(item.id) { + AccountTitle( + textColor = TangemTheme.colors.text.primary1, + textStyle = TangemTheme.typography.caption1, + accountTitleUM = item.title, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp) + .clip(shape) + .background(TangemTheme.colors.background.action) + .padding(start = 12.dp, end = 12.dp, top = 12.dp, bottom = 8.dp), + ) + } +} + +private fun LazyListScope.nftCollectionItem( + collection: NFTCollectionUM, + shape: RoundedCornerShape, + isNextItemNFTCollection: Boolean, +) { + item(key = collection.id) { + val itemShape = if (collection.isExpanded) { + shape.copy(bottomEnd = CornerSize(0.dp), bottomStart = CornerSize(0.dp)) + } else { + shape + } + NFTCollection( + modifier = Modifier + .clip(itemShape) + .background(TangemTheme.colors.background.action) + .fillMaxWidth(), + state = collection, + ) + } + when (val assets = collection.assets) { + is NFTCollectionAssetsListUM.Init -> Unit + is NFTCollectionAssetsListUM.Loading -> { + assetsListLoading( + collectionId = collection.id, + content = assets, + expanded = collection.isExpanded, + isNextItemNFTCollection = isNextItemNFTCollection, + ) + } + is NFTCollectionAssetsListUM.Failed -> { + assetsListFailed( + collectionId = collection.id, + content = assets, + expanded = collection.isExpanded, + isNextItemNFTCollection = isNextItemNFTCollection, + ) + } + is NFTCollectionAssetsListUM.Content -> { + assetsListContent( + collectionId = collection.id, + content = assets, + expanded = collection.isExpanded, + isNextItemNFTCollection = isNextItemNFTCollection, + ) + } + } +} + private fun LazyListScope.assetsListLoading( collectionId: String, content: NFTCollectionAssetsListUM.Loading, + isNextItemNFTCollection: Boolean, expanded: Boolean, ) { val itemsCount = content.itemsCount @@ -164,6 +240,8 @@ private fun LazyListScope.assetsListLoading( Row( modifier = Modifier .fillMaxWidth() + .clip(assetsShape(isNextItemNFTCollection)) + .background(TangemTheme.colors.background.action) .padding(TangemTheme.dimens.spacing6), ) { NFTCollectionAssetLoading( @@ -193,6 +271,7 @@ private fun LazyListScope.assetsListLoading( private fun LazyListScope.assetsListFailed( collectionId: String, content: NFTCollectionAssetsListUM.Failed, + isNextItemNFTCollection: Boolean, expanded: Boolean, ) { item( @@ -206,6 +285,8 @@ private fun LazyListScope.assetsListFailed( Box( modifier = Modifier .fillMaxWidth() + .clip(assetsShape(isNextItemNFTCollection)) + .background(TangemTheme.colors.background.action) .height(TangemTheme.dimens.size142), contentAlignment = Alignment.Center, ) { @@ -220,6 +301,7 @@ private fun LazyListScope.assetsListFailed( private fun LazyListScope.assetsListContent( collectionId: String, content: NFTCollectionAssetsListUM.Content, + isNextItemNFTCollection: Boolean, expanded: Boolean, ) { val items = content.items @@ -244,6 +326,10 @@ private fun LazyListScope.assetsListContent( ) Row( modifier = Modifier + .conditional(rowIndex.inc() == rowCount) { + clip(assetsShape(isNextItemNFTCollection)) + } + .background(TangemTheme.colors.background.action) .fillMaxWidth() .padding(TangemTheme.dimens.spacing6), ) { @@ -277,105 +363,150 @@ private fun LazyListScope.assetsListContent( } } +private fun assetsShape(isNextItemNFTCollection: Boolean): Shape = if (isNextItemNFTCollection) { + RoundedCornerShape(0.dp) +} else { + RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp) +} + @Suppress("LongMethod") @Preview(widthDp = 360, showBackground = true) @Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_NFTCollectionsContent() { +private fun Preview_NFTCollectionsContent(@PreviewParameter(PreviewProvider::class) state: NFTCollectionsUM.Content) { TangemThemePreview { NFTCollectionsContent( - content = NFTCollectionsUM.Content( - search = SearchBarUM( - placeholderText = resourceReference(R.string.common_search), - query = "", - onQueryChange = {}, - isActive = false, - onActiveChange = { }, + content = state, + ) + } +} + +private class PreviewProvider : PreviewParameterProvider { + + val search + get() = SearchBarUM( + placeholderText = resourceReference(R.string.common_search), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = { }, + ) + + val warnings + get() = persistentListOf( + NFTCollectionsWarningUM( + id = "loading troubles", + config = NotificationConfig( + title = TextReference.Res(R.string.nft_collections_warning_title), + subtitle = TextReference.Res(R.string.nft_collections_warning_subtitle), + iconResId = R.drawable.ic_alert_triangle_20, ), - collections = persistentListOf( - NFTCollectionUM( + ), + ) + + val collection + get() = NFTCollectionUM( + id = UUID.randomUUID().toString(), + name = "Nethers", + logoUrl = "", + networkIconId = R.drawable.img_eth_22, + description = TextReference.Str("3 items"), + assets = NFTCollectionAssetsListUM.Content(persistentListOf()), + isExpanded = false, + onExpandClick = { }, + ) + + val collectionLoading + get() = collection.copy( + assets = NFTCollectionAssetsListUM.Loading( + itemsCount = 1, + ), + isExpanded = true, + ) + + val collectionFailed + get() = collection.copy( + assets = NFTCollectionAssetsListUM.Failed( + onRetryClick = { }, + ), + isExpanded = true, + ) + + val collectionContent + get() = collection.copy( + assets = NFTCollectionAssetsListUM.Content( + items = persistentListOf( + NFTCollectionAssetUM( id = "item1", - name = "Nethers", - logoUrl = "", - networkIconId = R.drawable.img_eth_22, - description = TextReference.Str("3 items"), - assets = NFTCollectionAssetsListUM.Content(persistentListOf()), - isExpanded = false, - onExpandClick = { }, + name = "Nethers #0854", + imageUrl = "img", + price = NFTSalePriceUM.Content( + price = stringReference("0.05 ETH"), + ), + onItemClick = { }, ), - NFTCollectionUM( + NFTCollectionAssetUM( id = "item2", - name = "Nethers", - logoUrl = "", - networkIconId = R.drawable.img_eth_22, - description = TextReference.Str("3 items"), - assets = NFTCollectionAssetsListUM.Loading( - itemsCount = 1, - ), - isExpanded = true, - onExpandClick = { }, + name = "Nethers #0855", + imageUrl = "img", + price = NFTSalePriceUM.Loading, + onItemClick = { }, ), - NFTCollectionUM( + NFTCollectionAssetUM( id = "item3", - name = "Nethers", - logoUrl = "", - networkIconId = R.drawable.img_eth_22, - description = TextReference.Str("3 items"), - assets = NFTCollectionAssetsListUM.Failed( - onRetryClick = { }, - ), - isExpanded = true, - onExpandClick = { }, - ), - NFTCollectionUM( - id = "item4", - name = "Nethers", - logoUrl = "", - networkIconId = R.drawable.img_eth_22, - description = TextReference.Str("3 items"), - assets = NFTCollectionAssetsListUM.Content( - items = persistentListOf( - NFTCollectionAssetUM( - id = "item1", - name = "Nethers #0854", - imageUrl = "img", - price = NFTSalePriceUM.Content( - price = stringReference("0.05 ETH"), - ), - onItemClick = { }, - ), - NFTCollectionAssetUM( - id = "item2", - name = "Nethers #0855", - imageUrl = "img", - price = NFTSalePriceUM.Loading, - onItemClick = { }, - ), - NFTCollectionAssetUM( - id = "item3", - name = "Nethers #0856", - imageUrl = "img", - price = NFTSalePriceUM.Failed, - onItemClick = { }, - ), - ), - ), - isExpanded = true, - onExpandClick = { }, + name = "Nethers #0856", + imageUrl = "img", + price = NFTSalePriceUM.Failed, + onItemClick = { }, ), ), - warnings = persistentListOf( - NFTCollectionsWarningUM( - id = "loading troubles", - config = NotificationConfig( - title = TextReference.Res(R.string.nft_collections_warning_title), - subtitle = TextReference.Res(R.string.nft_collections_warning_subtitle), - iconResId = R.drawable.ic_alert_triangle_20, - ), - ), + ), + isExpanded = true, + ) + + val accountHeader + get() = NFTCollectionPortfolioUM( + title = AccountTitleUM.Account( + icon = AccountIconPreviewData.randomAccountIcon(), + name = stringReference("Main Account"), + prefixText = TextReference.EMPTY, + ), + id = UUID.randomUUID().toString(), + ) + + override val values: Sequence + get() = sequenceOf( + NFTCollectionsUM.Content( + search = search, + collections = persistentListOf( + collection, + collectionLoading, + collectionFailed, + collectionContent, ), + warnings = warnings, + onReceiveClick = { }, + ), + NFTCollectionsUM.Content( + search = search, + collections = persistentListOf( + accountHeader, + collection, + accountHeader, + collectionContent, + collection, + ), + warnings = warnings, + onReceiveClick = { }, + ), + NFTCollectionsUM.Content( + search = search, + collections = persistentListOf( + collectionContent, + collection, + ), + warnings = warnings, onReceiveClick = { }, ), ) - } } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt index 6aa2bc4a8f..6c9581913e 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt @@ -4,7 +4,12 @@ import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop @@ -13,7 +18,13 @@ import com.arkivanov.decompose.value.subscribe import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.models.PortfolioId +import com.tangem.features.account.PortfolioFetcher +import com.tangem.features.account.PortfolioSelectorComponent +import com.tangem.features.account.PortfolioSelectorController import com.tangem.features.nft.collections.NFTCollectionsComponent import com.tangem.features.nft.common.ui.NFTContent import com.tangem.features.nft.component.NFTComponent @@ -23,20 +34,26 @@ import com.tangem.features.nft.entity.NFTSendSuccessListener import com.tangem.features.nft.receive.NFTReceiveComponent import com.tangem.features.nft.traits.NFTAssetTraitsComponent import com.tangem.features.tokenreceive.TokenReceiveComponent +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.serialization.builtins.serializer +@Suppress("LongParameterList") internal class DefaultNFTComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: NFTComponent.Params, private val nftDetailsInfoComponentFactory: NFTDetailsInfoComponent.Factory, nftSendSuccessListener: NFTSendSuccessListener, private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, + private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory, + private val portfolioSelectorController: PortfolioSelectorController, + portfolioFetcherFactory: PortfolioFetcher.Factory, + private val accountsFeatureToggles: AccountsFeatureToggles, ) : NFTComponent, AppComponentContext by appComponentContext { private val stackNavigation = StackNavigation() @@ -48,6 +65,26 @@ internal class DefaultNFTComponent @AssistedInject constructor( private val initialRoute: NFTRoute = NFTRoute.Collections(params.userWalletId) private val currentRoute = MutableStateFlow(initialRoute) + private val onReceiveClickJob = JobHolder() + private val portfolioFetcher: PortfolioFetcher? = if (accountsFeatureToggles.isFeatureEnabled) { + portfolioFetcherFactory.create( + mode = PortfolioFetcher.Mode.Wallet(params.userWalletId), + scope = componentScope, + ) + } else { + null + } + private val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback { + override val onDismiss: () -> Unit = { bottomSheetNavigation.dismiss() } + override val onBack: () -> Unit = { bottomSheetNavigation.dismiss() } + } + private val bottomSheetSlot = childSlot( + source = bottomSheetNavigation, + serializer = Unit.serializer(), + handleBackButton = false, + childFactory = { configuration, context -> bottomSheetChild(context) }, + ) private val childStack = childStack( key = "sendInnerStack", @@ -90,6 +127,8 @@ internal class DefaultNFTComponent @AssistedInject constructor( NFTContent( stackState = stackState, ) + val bottomSheet by bottomSheetSlot.subscribeAsState() + bottomSheet.child?.instance?.BottomSheet() } private fun createChild(route: NFTRoute, factoryContext: AppComponentContext) = when (route) { @@ -108,11 +147,15 @@ internal class DefaultNFTComponent @AssistedInject constructor( userWalletId = route.userWalletId, onBackClick = ::onChildBack, onReceiveClick = { - innerRouter.push( - NFTRoute.Receive( - userWalletId = route.userWalletId, - ), - ) + if (accountsFeatureToggles.isFeatureEnabled) { + onReceiveClick(route) + } else { + innerRouter.push( + NFTRoute.Receive( + portfolioId = PortfolioId(route.userWalletId), + ), + ) + } }, onAssetClick = { asset, collection -> innerRouter.push( @@ -126,14 +169,31 @@ internal class DefaultNFTComponent @AssistedInject constructor( ), ) + private fun onReceiveClick(route: NFTRoute.Collections) = componentScope.launch { + val portfolioFetcher = requireNotNull(portfolioFetcher) + portfolioSelectorController.selectAccount(null) + portfolioFetcher.updateMode(mode = PortfolioFetcher.Mode.Wallet(route.userWalletId)) + val portfolioData = portfolioFetcher.data.first() + if (portfolioData.isSingleChoice) { + val mainAccountId = portfolioData.balances.values.first() + .accountsBalance.mainAccount.account.accountId + innerRouter.push(NFTRoute.Receive(portfolioId = PortfolioId(mainAccountId))) + } else { + bottomSheetNavigation.activate(Unit) + val selectedAccountId = portfolioSelectorController.selectedAccount + .filterNotNull().first() + bottomSheetNavigation.dismiss() + innerRouter.push(NFTRoute.Receive(portfolioId = PortfolioId(selectedAccountId))) + } + }.saveIn(onReceiveClickJob) + private fun getReceiveComponent( factoryContext: AppComponentContext, route: NFTRoute.Receive, ): ComposableContentComponent = NFTReceiveComponent( context = factoryContext, params = NFTReceiveComponent.Params( - userWalletId = route.userWalletId, - walletName = params.walletName, + portfolioId = route.portfolioId, onBackClick = ::onChildBack, ), tokenReceiveComponentFactory = tokenReceiveComponentFactory, @@ -181,6 +241,16 @@ internal class DefaultNFTComponent @AssistedInject constructor( } } + private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent = + portfolioSelectorComponentFactory.create( + context = childByContext(componentContext), + params = PortfolioSelectorComponent.Params( + portfolioFetcher = portfolioFetcher!!, + controller = portfolioSelectorController, + bsCallback = portfolioSelectorCallback, + ), + ) + @AssistedFactory interface Factory : NFTComponent.Factory { override fun create(context: AppComponentContext, params: NFTComponent.Params): DefaultNFTComponent diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/NFTRoute.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/NFTRoute.kt index 56bad033c8..aa810f1b81 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/NFTRoute.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/NFTRoute.kt @@ -1,6 +1,7 @@ package com.tangem.features.nft.common import com.tangem.core.decompose.navigation.Route +import com.tangem.domain.models.PortfolioId import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.models.wallet.UserWalletId @@ -15,7 +16,7 @@ internal sealed class NFTRoute : Route { @Serializable data class Receive( - val userWalletId: UserWalletId, + val portfolioId: PortfolioId, ) : NFTRoute() @Serializable diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt index e4bd9a112e..560eee70b5 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt @@ -13,8 +13,8 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.TokenReceiveConfig -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.nft.receive.model.NFTReceiveModel import com.tangem.features.nft.receive.ui.NFTReceive import com.tangem.features.tokenreceive.TokenReceiveComponent @@ -57,8 +57,7 @@ internal class NFTReceiveComponent @AssistedInject constructor( ) data class Params( - val userWalletId: UserWalletId, - val walletName: String, + val portfolioId: PortfolioId, val onBackClick: () -> Unit, ) } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt index 1a9c902d2a..e9ebb194f0 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.nft.receive.model import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate +import com.tangem.common.ui.account.toUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -11,19 +12,28 @@ import com.tangem.core.navigation.share.ShareManager import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.fields.InputManager import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.account.producer.SingleAccountProducer +import com.tangem.domain.account.supplier.SingleAccountSupplier +import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase +import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.TokenReceiveConfig +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.FilterNFTAvailableNetworksUseCase import com.tangem.domain.nft.GetNFTCurrencyUseCase import com.tangem.domain.nft.GetNFTNetworkStatusUseCase import com.tangem.domain.nft.GetNFTNetworksUseCase import com.tangem.domain.nft.analytics.NFTAnalyticsEvent import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.nft.impl.R import com.tangem.features.nft.receive.NFTReceiveComponent import com.tangem.features.nft.receive.entity.NFTReceiveUM @@ -53,6 +63,9 @@ internal class NFTReceiveModel @Inject constructor( private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, private val getNFTCurrencyUseCase: GetNFTCurrencyUseCase, private val receiveAddressesFactory: ReceiveAddressesFactory, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val singleAccountSupplier: SingleAccountSupplier, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -63,10 +76,7 @@ internal class NFTReceiveModel @Inject constructor( private val _state = MutableStateFlow( value = NFTReceiveUM( onBackClick = params.onBackClick, - appBarSubtitle = resourceReference( - R.string.hot_crypto_add_token_subtitle, - formatArgs = wrappedList(params.walletName), - ), + appBarSubtitle = TextReference.EMPTY, search = getInitialSearchBar(), networks = NFTReceiveUM.Networks.Content( availableItems = persistentListOf(), @@ -81,11 +91,44 @@ internal class NFTReceiveModel @Inject constructor( init { analyticsEventHandler.send(NFTAnalyticsEvent.Receive.ScreenOpened) subscribeToNFTAvailableNetworks() + loadPortfolioName() } + private fun loadPortfolioName() = modelScope.launch(dispatchers.default) { + val appBarSubtitle = when (val portfolioId = params.portfolioId) { + is PortfolioId.Wallet -> loadWalletName(portfolioId.userWalletId) + is PortfolioId.Account -> if (isAccountsModeEnabledUseCase.invokeSync()) { + loadAccountName(portfolioId.accountId) + } else { + loadWalletName(portfolioId.userWalletId) + } + } + _state.update { it.copy(appBarSubtitle = appBarSubtitle) } + } + + private fun loadWalletName(userWalletId: UserWalletId): TextReference { + return getUserWalletUseCase(userWalletId) + .map { it.name } + .getOrNull() + ?.let { createAppBarSubtitle(stringReference(it)) } + ?: TextReference.EMPTY + } + + private suspend fun loadAccountName(accountId: AccountId): TextReference { + return singleAccountSupplier + .getSyncOrNull(SingleAccountProducer.Params(accountId)) + ?.let { createAppBarSubtitle(it.accountName.toUM().value) } + ?: TextReference.EMPTY + } + + private fun createAppBarSubtitle(text: TextReference) = resourceReference( + R.string.hot_crypto_add_token_subtitle, + formatArgs = wrappedList(text), + ) + private fun subscribeToNFTAvailableNetworks() { combine( - flow = getNFTNetworksUseCase(params.userWalletId), + flow = getNFTNetworksUseCase(params.portfolioId), flow2 = searchManager.query.distinctUntilChanged(), ) { networks, query -> filterNFTAvailableNetworksUseCase(networks, query) @@ -98,6 +141,7 @@ internal class NFTReceiveModel @Inject constructor( ).transform(it) } } + .flowOn(dispatchers.default) .launchIn(modelScope) } @@ -144,7 +188,7 @@ internal class NFTReceiveModel @Inject constructor( analyticsEventHandler.send(NFTAnalyticsEvent.Receive.BlockchainChosen(network.name)) val networkStatus = getNFTNetworkStatusUseCase.invoke( - userWalletId = params.userWalletId, + userWalletId = params.portfolioId.userWalletId, network = network, ) ?: return@launch @@ -191,7 +235,7 @@ internal class NFTReceiveModel @Inject constructor( private suspend fun configureReceiveAddresses(addresses: NetworkAddress, network: Network): TokenReceiveConfig { val cryptoCurrency = getNFTCurrencyUseCase.invoke(network) return receiveAddressesFactory.createForNft( - userWalletId = params.userWalletId, + userWalletId = params.portfolioId.userWalletId, addresses = addresses, network = network, nft = cryptoCurrency, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt index 9f47e3e882..fc7ca61d39 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt @@ -20,7 +20,7 @@ internal class ExpandedAccountsHolder @Inject constructor( fun expandedAccounts(userWallet: UserWallet): Flow> = channelFlow { walletAccounts(userWallet) .onEach { accountList -> - val isSingleAccount = accountList.totalAccounts == 1 + val isSingleAccount = accountList.accounts.size == 1 val defaultExpanded = when { isSingleAccount -> setOf(accountList.mainAccount.accountId) else -> setOf()