From fb2f23c8a9a4cc2742992a48d9c4c69267699a23 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Mar 2025 15:59:22 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 40 +++ .../tap/di/domain/WalletsDomainModule.kt | 14 + .../local/nft/DefaultNFTPersistenceStore.kt | 2 +- .../local/nft/NFTPersistenceStore.kt | 2 +- .../local/nft/NFTPersistenceStoreFactory.kt | 77 +++-- .../nft/converter/NFTSdkAssetConverter.kt | 9 +- .../converter/NFTSdkCollectionConverter.kt | 9 +- .../tangem/data/nft/DefaultNFTRepository.kt | 284 ++++++++++-------- .../data/wallets/DefaultWalletsRepository.kt | 4 + .../domain/nft/models/NFTCollections.kt | 10 + .../domain/nft/FetchNFTCollectionsUseCase.kt | 16 + .../domain/nft/GetNFTCollectionsUseCase.kt | 25 ++ .../wallets/repository/WalletsRepository.kt | 2 + features/nft/impl/build.gradle.kts | 3 + features/wallet/impl/build.gradle.kts | 3 + .../wallet/child/wallet/model/WalletModel.kt | 35 ++- .../model/WalletsUpdateActionResolver.kt | 5 +- .../model/intents/WalletClickIntents.kt | 8 + .../common/preview/WalletScreenPreviewData.kt | 6 + .../domain/IsWalletNFTEnabledSyncUseCase.kt | 22 ++ .../implementors/MultiWalletContentLoader.kt | 15 + .../MultiWalletContentLoaderFactory.kt | 9 + .../wallet/state/WalletStateController.kt | 1 + .../wallet/state/model/WalletState.kt | 3 + .../InitializeWalletsTransformer.kt | 4 +- .../ReinitializeWalletTransformer.kt | 8 +- .../RemoveNFTCollectionsTransformer.kt | 23 ++ .../SetNFTCollectionsTransformer.kt | 95 ++++++ .../transformers/UnlockWalletTransformer.kt | 4 +- .../state/utils/WalletLoadingStateFactory.kt | 10 +- .../subscribers/WalletNFTListSubscriber.kt | 50 +++ .../presentation/wallet/ui/WalletScreen.kt | 13 + .../wallet/ui/components/WalletNFTItem.kt | 34 ++- .../MultiCurrencyNFTCollections.kt | 18 ++ gradle/tangem_dependencies.toml | 2 +- 35 files changed, 674 insertions(+), 191 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.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 new file mode 100644 index 0000000000..039fa951ff --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -0,0 +1,40 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.nft.FetchNFTCollectionsUseCase +import com.tangem.domain.nft.GetNFTCollectionsUseCase +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object NFTDomainModule { + + @Provides + @Singleton + fun providesGetNFTCollectionsUseCase( + currenciesRepository: CurrenciesRepository, + nftRepository: NFTRepository, + ): GetNFTCollectionsUseCase { + return GetNFTCollectionsUseCase( + currenciesRepository = currenciesRepository, + nftRepository = nftRepository, + ) + } + + @Provides + @Singleton + fun providesFetchNFTCollectionsUseCase( + currenciesRepository: CurrenciesRepository, + nftRepository: NFTRepository, + ): FetchNFTCollectionsUseCase { + return FetchNFTCollectionsUseCase( + currenciesRepository = currenciesRepository, + nftRepository = nftRepository, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 9ede57293c..9be71f685e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -10,8 +10,10 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* +import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.operations.attestation.OnlineCardVerifier +import com.tangem.features.nft.NFTFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -173,4 +175,16 @@ internal object WalletsDomainModule { fun provideGetCardImageUseCase(onlineCardVerifier: OnlineCardVerifier): GetCardImageUseCase { return GetCardImageUseCase(verifier = onlineCardVerifier) } + + @Provides + @Singleton + fun providesIsWalletNFTEnabledSyncUseCase( + walletsRepository: WalletsRepository, + nftFeatureToggles: NFTFeatureToggles, + ): IsWalletNFTEnabledSyncUseCase { + return IsWalletNFTEnabledSyncUseCase( + walletsRepository = walletsRepository, + nftFeatureToggles = nftFeatureToggles, + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt index 43d82aaa8d..a418b31a5d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt @@ -12,7 +12,7 @@ internal class DefaultNFTPersistenceStore( private val pricesPersistenceStore: DataStore>, ) : NFTPersistenceStore { - override fun getCollections(): Flow> = collectionsPersistenceStore.data + override fun getCollections(): Flow?> = collectionsPersistenceStore.data override suspend fun getCollectionsSync(): List? = collectionsPersistenceStore .data diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt index ecd7275a6a..3a6b27d9f3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt @@ -5,7 +5,7 @@ import com.tangem.blockchain.nft.models.NFTCollection import kotlinx.coroutines.flow.Flow interface NFTPersistenceStore { - fun getCollections(): Flow> + fun getCollections(): Flow?> suspend fun getCollectionsSync(): List? diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt index 49d0ce8415..cb2e6399bf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.local.nft import android.content.Context +import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi @@ -11,10 +12,12 @@ import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.listTypes import com.tangem.datasource.utils.mapWithCustomKeyTypes import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob +import java.lang.reflect.ParameterizedType import javax.inject.Inject import javax.inject.Singleton @@ -25,41 +28,51 @@ class NFTPersistenceStoreFactory @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) { - fun provide(network: Network): NFTPersistenceStore { - val networkStringIdentifier = listOfNotNull( - network.id.value, - network.derivationPath.value, - ).joinToString("_") { - // remove all non-alphanumeric characters - it - .toCharArray() - .filter(Char::isLetterOrDigit) - .joinToString("") - .lowercase() - } + fun provide(userWalletId: UserWalletId, network: Network): NFTPersistenceStore { + // simplify network identifier so that is correct for a file name + // e.g. eth_m4460000 or theopennetwork_m446070 + val networkStringId = + network.id.formatted() + network.derivationPath.formatted()?.let { "_$it" }.orEmpty() + // simplify user wallet id so that is correct for a file name + // e.g. 9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a + val userWalletStringId = userWalletId.formatted() return DefaultNFTPersistenceStore( - collectionsPersistenceStore = DataStoreFactory.create( - serializer = MoshiDataStoreSerializer( - moshi = moshi, - types = listTypes(), - defaultValue = emptyList(), - ), - produceFile = { - context.dataStoreFile(fileName = "nft_${networkStringIdentifier}_collections") - }, - scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + collectionsPersistenceStore = createPersistenceStore( + // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_collections + // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_collections + fileName = "nft_${userWalletStringId}_${networkStringId}_collections", + types = listTypes(), + defaultValue = emptyList(), ), - pricesPersistenceStore = DataStoreFactory.create( - serializer = MoshiDataStoreSerializer( - moshi = moshi, - types = mapWithCustomKeyTypes(), - defaultValue = emptyMap(), - ), - produceFile = { - context.dataStoreFile(fileName = "nft_${networkStringIdentifier}_prices") - }, - scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + pricesPersistenceStore = createPersistenceStore( + // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_prices + // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_prices + fileName = "nft_${userWalletStringId}_${networkStringId}_prices", + types = mapWithCustomKeyTypes(), + defaultValue = emptyMap(), ), ) } + + private fun createPersistenceStore(fileName: String, types: ParameterizedType, defaultValue: T): DataStore = + DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = types, + defaultValue = defaultValue, + ), + produceFile = { context.dataStoreFile(fileName = fileName) }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ) + + private fun Network.ID.formatted(): String = value + .filter(Char::isLetterOrDigit) + .lowercase() + + private fun Network.DerivationPath.formatted(): String? = value + ?.filter(Char::isLetterOrDigit) + ?.lowercase() + + private fun UserWalletId.formatted(): String = stringValue + .lowercase() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt index ce67e88dd6..add09e2c0a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt @@ -7,14 +7,11 @@ import com.tangem.domain.tokens.model.Network import com.tangem.utils.converter.Converter import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset -class NFTSdkAssetConverter( - private val nftSdkAssetIdentifierConverter: NFTSdkAssetIdentifierConverter, - private val nftSdkCollectionIdentifierConverter: NFTSdkCollectionIdentifierConverter, -) : Converter, NFTAsset> { +object NFTSdkAssetConverter : Converter, NFTAsset> { override fun convert(value: Pair): NFTAsset { val (network, asset) = value - val assetId = nftSdkAssetIdentifierConverter.convert(asset.identifier) - val collectionId = nftSdkCollectionIdentifierConverter.convert(asset.collectionIdentifier) + val assetId = NFTSdkAssetIdentifierConverter.convert(asset.identifier) + val collectionId = NFTSdkCollectionIdentifierConverter.convert(asset.collectionIdentifier) return NFTAsset.Value( id = assetId, collectionId = collectionId, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt index 7b1be28a4e..ed9a5ed3ec 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt @@ -6,13 +6,10 @@ import com.tangem.domain.tokens.model.Network import com.tangem.utils.converter.Converter import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection -class NFTSdkCollectionConverter( - private val nftSdkCollectionIdentifierConverter: NFTSdkCollectionIdentifierConverter, - private val nftSdkAssetConverter: NFTSdkAssetConverter, -) : Converter, NFTCollection> { +object NFTSdkCollectionConverter : Converter, NFTCollection> { override fun convert(value: Pair): NFTCollection { val (network, collection) = value - val collectionId = nftSdkCollectionIdentifierConverter.convert(collection.identifier) + val collectionId = NFTSdkCollectionIdentifierConverter.convert(collection.identifier) return NFTCollection( id = collectionId, network = network, @@ -22,7 +19,7 @@ class NFTSdkCollectionConverter( count = collection.count, assets = collection.assets .map { asset -> - nftSdkAssetConverter.convert(network to asset) + NFTSdkAssetConverter.convert(network to asset) } .filter { it.id !is NFTAsset.Identifier.Unknown diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index f0c4c72fea..5a89fbe57f 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -1,14 +1,14 @@ package com.tangem.data.nft import arrow.core.Either +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.datasource.local.nft.NFTPersistenceStore import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory import com.tangem.datasource.local.nft.NFTRuntimeStore import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory -import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter import com.tangem.datasource.local.nft.converter.NFTSdkAssetIdentifierConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter -import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter import com.tangem.domain.models.StatusSource import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections @@ -20,8 +20,10 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn -import kotlinx.coroutines.* +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch import javax.inject.Inject import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection @@ -34,82 +36,82 @@ internal class DefaultNFTRepository @Inject constructor( private val jobs = mutableMapOf() - private val nftRuntimeStores = mutableMapOf() - private val nftPersistenceStores = mutableMapOf() - - private val nftSdkAssetConverter = NFTSdkAssetConverter( - nftSdkAssetIdentifierConverter = NFTSdkAssetIdentifierConverter, - nftSdkCollectionIdentifierConverter = NFTSdkCollectionIdentifierConverter, - ) - private val collectionConverter = NFTSdkCollectionConverter( - nftSdkCollectionIdentifierConverter = NFTSdkCollectionIdentifierConverter, - nftSdkAssetConverter = nftSdkAssetConverter, - ) + private val nftRuntimeStores = mutableMapOf() + private val nftPersistenceStores = mutableMapOf() override fun observeCollections(userWalletId: UserWalletId, networks: List): Flow> = - flow { - emitAll( - combine( - networks.map { getNFTRuntimeStore(it).getCollections() }, - ) { it.asList() }, - ) - }.onStart { - refreshCollections(userWalletId, networks) - } + flow { emitAll(observeCollectionsInternal(userWalletId, networks)) } + + private suspend fun observeCollectionsInternal( + userWalletId: UserWalletId, + networks: List, + ): Flow> = combine( + networks.map { observeCollectionsInternal(userWalletId, it) }, + ) { it.asList() } + + private suspend fun observeCollectionsInternal( + userWalletId: UserWalletId, + network: Network, + ): Flow = if (network.canHandleNFTs()) { + getNFTRuntimeStore(userWalletId, network).getCollections() + } else { + flowOf(NFTCollections.empty(network)) + } override suspend fun refreshCollections(userWalletId: UserWalletId, networks: List) = coroutineScope { - networks.map { network -> - launch(dispatchers.io) { - Either.catch { - expireCollections(network) - walletManagersFacade.getNFTCollections(userWalletId, network) - }.onLeft { - saveFailedStateInRuntime( - network = network, - error = it, - ) - }.onRight { - val mergedCollections = it.mergeWithStoredAssets(network) + networks.mapNotNull { network -> + if (network.canHandleNFTs()) { + launch(dispatchers.io) { + Either.catch { + expireCollections(userWalletId, network) + walletManagersFacade.getNFTCollections(userWalletId, network) + }.onLeft { + saveFailedStateInRuntime( + userWalletId = userWalletId, + network = network, + error = it, + ) + }.onRight { + val mergedCollections = it.mergeWithStoredAssets(userWalletId, network) - saveCollectionsInRuntime( - network = network, - collections = mergedCollections, - ) - saveCollectionsInPersistence( - network = network, - collections = mergedCollections, - ) - } - }.saveIn(getJobHolder(network)) + saveCollectionsInRuntime( + userWalletId = userWalletId, + network = network, + collections = mergedCollections, + ) + saveCollectionsInPersistence( + userWalletId = userWalletId, + network = network, + collections = mergedCollections, + ) + } + }.saveIn(getJobHolder(network)) + } else { + null + } }.joinAll() } - private suspend fun expireCollections(network: Network) { - val runtimeStore = getNFTRuntimeStore(network) - + private suspend fun expireCollections(userWalletId: UserWalletId, network: Network) { + val runtimeStore = getNFTRuntimeStore(userWalletId, network) val expiredCollections = runtimeStore .getCollectionsSync() - .let { collections -> - collections.copy( - content = when (val content = collections.content) { - is NFTCollections.Content.Collections -> content.copy( - source = StatusSource.CACHE, - ) - is NFTCollections.Content.Error -> content - }, - ) - } + .changeStatusSource(StatusSource.CACHE) runtimeStore.saveCollections(expiredCollections) } - private suspend fun saveCollectionsInRuntime(network: Network, collections: List) { - getNFTRuntimeStore(network).saveCollections( + private suspend fun saveCollectionsInRuntime( + userWalletId: UserWalletId, + network: Network, + collections: List, + ) { + getNFTRuntimeStore(userWalletId, network).saveCollections( NFTCollections( network = network, content = NFTCollections.Content.Collections( collections = collections .map { collection -> - collectionConverter.convert(network to collection) + NFTSdkCollectionConverter.convert(network to collection) } .filter { it.id !is NFTCollection.Identifier.Unknown @@ -120,19 +122,30 @@ internal class DefaultNFTRepository @Inject constructor( ) } - private suspend fun saveFailedStateInRuntime(network: Network, error: Throwable) { - getNFTRuntimeStore(network).saveCollections( - NFTCollections( - network = network, - content = NFTCollections.Content.Error( - error = error, - ), - ), - ) + private suspend fun saveFailedStateInRuntime(userWalletId: UserWalletId, network: Network, error: Throwable) { + getNFTRuntimeStore(userWalletId, network).let { store -> + val storedCollections = store.getCollectionsSync() + val content = storedCollections.content + val updatedCollections = if (content is NFTCollections.Content.Collections && content.collections != null) { + // if there is any cached collections in store, then mark them as not actual and emit anyway + storedCollections.changeStatusSource(StatusSource.ONLY_CACHE) + } else { + // otherwise, just emit an error + NFTCollections( + network = network, + content = NFTCollections.Content.Error(error), + ) + } + store.saveCollections(updatedCollections) + } } - private suspend fun saveCollectionsInPersistence(network: Network, collections: List) { - getNFTPersistenceStore(network).saveCollections(collections) + private suspend fun saveCollectionsInPersistence( + userWalletId: UserWalletId, + network: Network, + collections: List, + ) { + getNFTPersistenceStore(userWalletId, network).saveCollections(collections) } private fun getJobHolder(network: Network): JobHolder = jobs[network] ?: run { @@ -141,65 +154,85 @@ internal class DefaultNFTRepository @Inject constructor( } } - private fun getNFTPersistenceStore(network: Network): NFTPersistenceStore = nftPersistenceStores[network] ?: run { - nftPersistenceStoreFactory.provide(network).also { - nftPersistenceStores[network] = it + private fun getNFTPersistenceStore(userWalletId: UserWalletId, network: Network): NFTPersistenceStore { + val storeId = (userWalletId to network).formatted() + return nftPersistenceStores[storeId] ?: run { + nftPersistenceStoreFactory.provide(userWalletId, network).also { + nftPersistenceStores[storeId] = it + } } } - private suspend fun getNFTRuntimeStore(network: Network): NFTRuntimeStore = nftRuntimeStores[network] ?: run { - nftRuntimeStoreFactory.provide(network).also { - nftRuntimeStores[network] = it - val storedCollections = getStoredCollections(network) - val storedPrices = getStoredPrices(network) - it.initialize( - collections = storedCollections, - prices = storedPrices, - ) + private suspend fun getNFTRuntimeStore(userWalletId: UserWalletId, network: Network): NFTRuntimeStore { + val storeId = (userWalletId to network).formatted() + return nftRuntimeStores[storeId] ?: run { + nftRuntimeStoreFactory.provide(network).also { + nftRuntimeStores[storeId] = it + val storedCollections = getStoredCollections(userWalletId, network) + val storedPrices = getStoredPrices(userWalletId, network) + it.initialize( + collections = storedCollections, + prices = storedPrices, + ) + } } } - private suspend fun getStoredCollections(network: Network) = getNFTPersistenceStore(network) - .getCollectionsSync() - .let { - NFTCollections( - network = network, - content = NFTCollections.Content.Collections( - collections = it - ?.map { collection -> - collectionConverter.convert(network to collection) - } - ?.filter { - it.id !is NFTCollection.Identifier.Unknown - }, - source = StatusSource.CACHE, - ), - ) - } - - private suspend fun getStoredPrices(network: Network) = getNFTPersistenceStore(network) - .getSalePricesSync() - .orEmpty() - .let { prices -> - prices - .mapKeys { - val (assetId, _) = it - NFTSdkAssetIdentifierConverter.convert(assetId) - } - .mapValues { - val (assetId, price) = it - NFTSalePrice.Value( - assetId = assetId, - value = price.value, - symbol = price.symbol, + private suspend fun getStoredCollections(userWalletId: UserWalletId, network: Network) = + getNFTPersistenceStore(userWalletId, network) + .getCollectionsSync() + .let { + NFTCollections( + network = network, + content = NFTCollections.Content.Collections( + collections = it + ?.map { collection -> + NFTSdkCollectionConverter.convert(network to collection) + } + ?.filter { + it.id !is NFTCollection.Identifier.Unknown + }, source = StatusSource.CACHE, - ) - } - } + ), + ) + } - private suspend fun List.mergeWithStoredAssets(network: Network): List { + private suspend fun getStoredPrices(userWalletId: UserWalletId, network: Network) = + getNFTPersistenceStore(userWalletId, network) + .getSalePricesSync() + .orEmpty() + .let { prices -> + prices + .mapKeys { + val (assetId, _) = it + NFTSdkAssetIdentifierConverter.convert(assetId) + } + .mapValues { + val (assetId, price) = it + NFTSalePrice.Value( + assetId = assetId, + value = price.value, + symbol = price.symbol, + source = StatusSource.CACHE, + ) + } + } + + private fun NFTCollections.changeStatusSource(source: StatusSource) = copy( + content = when (val content = content) { + is NFTCollections.Content.Collections -> content.copy( + source = source, + ) + is NFTCollections.Content.Error -> content + }, + ) + + private suspend fun List.mergeWithStoredAssets( + userWalletId: UserWalletId, + network: Network, + ): List { val storedCollections = - getNFTPersistenceStore(network) + getNFTPersistenceStore(userWalletId, network) .getCollectionsSync() .orEmpty() .associateBy(SdkNFTCollection::identifier) @@ -224,4 +257,11 @@ internal class DefaultNFTRepository @Inject constructor( } } } + + private fun Pair.formatted(): String { + val (walletId, network) = this + return walletId.stringValue + "_" + network.id.value + "_" + network.derivationPath.value + } + + private fun Network.canHandleNFTs(): Boolean = Blockchain.fromNetworkId(backendId)?.canHandleNFTs() == true } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index e17b2c880c..35bc64c215 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -208,6 +208,10 @@ internal class DefaultWalletsRepository( .getObjectMap(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY) .map { it[userWalletId.stringValue] == true } + override fun nftEnabledStatuses(): Flow> = appPreferencesStore + .getObjectMap(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY) + .map { it.mapKeys { UserWalletId(it.key) } } + override suspend fun enableNFT(userWalletId: UserWalletId) { appPreferencesStore.editData { it.setObjectMap( diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt index b4e4accc54..bd97bf4cfe 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt @@ -17,4 +17,14 @@ data class NFTCollections( val error: Throwable, ) : Content() } + + companion object { + fun empty(network: Network) = NFTCollections( + network = network, + content = Content.Collections( + collections = null, + source = StatusSource.ACTUAL, + ), + ) + } } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt new file mode 100644 index 0000000000..e8ecd813c0 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.nft + +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId + +class FetchNFTCollectionsUseCase( + private val currenciesRepository: CurrenciesRepository, + private val nftRepository: NFTRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId) { + val currencies = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) + nftRepository.refreshCollections(userWalletId, currencies.map { it.network }.distinct()) + } +} \ 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 new file mode 100644 index 0000000000..f59d8578b6 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.nft + +import com.tangem.domain.nft.models.NFTCollections +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest + +class GetNFTCollectionsUseCase( + private val currenciesRepository: CurrenciesRepository, + private val nftRepository: NFTRepository, +) { + + @OptIn(ExperimentalCoroutinesApi::class) + fun launch(userWalletId: UserWalletId): Flow> = currenciesRepository + .getWalletCurrenciesUpdates(userWalletId) + .flatMapLatest { + val networks = it + .map { cryptoCurrency -> cryptoCurrency.network } + .distinct() + nftRepository.observeCollections(userWalletId, networks) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index b1e526ce5c..6ec22a4eaf 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -32,6 +32,8 @@ interface WalletsRepository { fun nftEnabledStatus(userWalletId: UserWalletId): Flow + fun nftEnabledStatuses(): Flow> + suspend fun enableNFT(userWalletId: UserWalletId) suspend fun disableNFT(userWalletId: UserWalletId) diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index 40d3b149c9..7c051fa34e 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -26,6 +26,9 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.datasource) + /** Domain modules */ + implementation(projects.domain.nft.models) + /** Common */ implementation(projects.common.ui) implementation(projects.common.routing) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index f786056d52..8416888824 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -90,6 +90,8 @@ dependencies { implementation(projects.domain.onramp) implementation(projects.domain.promo) implementation(projects.domain.promo.models) + implementation(projects.domain.nft) + implementation(projects.domain.nft.models) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) @@ -108,6 +110,7 @@ dependencies { implementation(projects.features.wallet.api) implementation(projects.features.walletSettings.api) implementation(projects.features.biometry.api) + implementation(projects.features.nft.api) /** Common modules */ implementation(projects.common) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 590d0d0bec..7848653a92 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -10,9 +10,11 @@ import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.nft.FetchNFTCollectionsUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -41,6 +43,7 @@ import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -76,6 +79,8 @@ internal class WalletModel @Inject constructor( private val walletFeatureToggles: WalletFeatureToggles, private val biometryFeatureToggles: BiometryFeatureToggles, private val analyticsEventsHandler: AnalyticsEventHandler, + private val fetchNFTCollectionsUseCase: FetchNFTCollectionsUseCase, + private val walletsRepository: WalletsRepository, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -86,6 +91,7 @@ internal class WalletModel @Inject constructor( private val walletsUpdateJobHolder = JobHolder() private val refreshWalletJobHolder = JobHolder() private val expressStatusJobHolder = JobHolder() + private val walletsNFTsUpdateJobHolder = JobHolder() private var needToRefreshWallet = false private var expressTxStatusTaskScheduler = SingleTaskScheduler() @@ -103,6 +109,7 @@ internal class WalletModel @Inject constructor( subscribeToScreenBackgroundState() subscribeOnPushNotificationsPermission() subscribeOnExpressTransactionsUpdates() + subscribeOnNFTUpdates() clickIntents.initialize(innerWalletRouter, modelScope) } @@ -160,7 +167,10 @@ internal class WalletModel @Inject constructor( .conflate() .distinctUntilChanged() .map { - walletsUpdateActionResolver.resolve(wallets = it, currentState = stateHolder.value) + walletsUpdateActionResolver.resolve( + wallets = it, + currentState = stateHolder.value, + ) } .onEach(::updateWallets) .flowOn(dispatchers.main) @@ -256,6 +266,29 @@ internal class WalletModel @Inject constructor( }.saveIn(expressStatusJobHolder) } + @OptIn(ExperimentalCoroutinesApi::class) + private fun subscribeOnNFTUpdates() { + getWalletsUseCase() + .conflate() + .distinctUntilChanged() + .flatMapLatest { wallets -> + wallets + .map { wallet -> + walletsRepository.nftEnabledStatus(wallet.walletId) + .distinctUntilChanged() + .onEach { nftEnabled -> + if (nftEnabled) { + fetchNFTCollectionsUseCase.invoke(wallet.walletId) + } + } + } + .merge() + } + .flowOn(dispatchers.main) + .launchIn(modelScope) + .saveIn(walletsNFTsUpdateJobHolder) + } + private fun needToRefreshTimer() { modelScope.launch { delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index 5594553aab..35cf3a44cd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -231,7 +231,10 @@ internal class WalletsUpdateActionResolver @Inject constructor( * @property prevWalletId previous selected wallet id * @property selectedWallet selected wallet */ - data class ReinitializeWallet(val prevWalletId: UserWalletId, val selectedWallet: UserWallet) : Action() { + data class ReinitializeWallet( + val prevWalletId: UserWalletId, + val selectedWallet: UserWallet, + ) : Action() { override fun toString(): String { return "ReinitializeWallet(prevWalletId = $prevWalletId, selectedWallet = ${selectedWallet.walletId})" diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index e84eccf060..a78fac8554 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -5,6 +5,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.nft.FetchNFTCollectionsUseCase import com.tangem.domain.onramp.FetchHotCryptoUseCase import com.tangem.domain.settings.NeverToShowWalletsScrollPreview import com.tangem.domain.tokens.FetchCardTokenListUseCase @@ -14,6 +15,7 @@ import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -50,6 +52,8 @@ internal class WalletClickIntents @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val onrampFeatureToggles: OnrampFeatureToggles, private val fetchHotCryptoUseCase: FetchHotCryptoUseCase, + private val fetchNFTCollectionsUseCase: FetchNFTCollectionsUseCase, + private val isWalletNFTEnabledSyncUseCase: IsWalletNFTEnabledSyncUseCase, ) : BaseWalletClickIntents(), WalletCardClickIntents by walletCardClickIntentsImplementor, WalletWarningsClickIntents by warningsClickIntentsImplementer, @@ -140,6 +144,10 @@ internal class WalletClickIntents @Inject constructor( if (onrampFeatureToggles.isHotTokensEnabled) { async { fetchHotCryptoUseCase() }.let(::add) } + + if (isWalletNFTEnabledSyncUseCase.invoke(userWallet.walletId)) { + async { fetchNFTCollectionsUseCase(userWalletId = userWallet.walletId) }.let(::add) + } } .awaitAll() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 4f8cb9bdfd..b8cf556b05 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -120,6 +120,12 @@ internal object WalletScreenPreviewData { ), bottomSheetConfig = null, tokensListState = textContentTokensState, + nftState = WalletNFTItemUM.Content( + previews = persistentListOf(WalletNFTItemUM.Content.CollectionPreview.Image("img1")), + collectionsCount = 1, + assetsCount = 3, + isFlickering = false, + ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt new file mode 100644 index 0000000000..e931183ffe --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.nft.NFTFeatureToggles +import kotlinx.coroutines.flow.firstOrNull + +class IsWalletNFTEnabledSyncUseCase( + private val walletsRepository: WalletsRepository, + private val nftFeatureToggles: NFTFeatureToggles, +) { + + suspend operator fun invoke(userWalletId: UserWalletId): Boolean = if (nftFeatureToggles.isNFTEnabled) { + walletsRepository + .nftEnabledStatuses() + .firstOrNull() + ?.let { it[userWalletId] } + ?: false + } else { + false + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 402b03dec0..c69025ad0a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -3,10 +3,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -21,6 +23,7 @@ import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletActi import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletTokenListSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber +import com.tangem.features.nft.NFTFeatureToggles import com.tangem.features.swap.SwapFeatureToggles @Suppress("LongParameterList") @@ -34,6 +37,7 @@ internal class MultiWalletContentLoader( private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, private val walletWithFundsChecker: WalletWithFundsChecker, private val tokenListStore: MultiWalletTokenListStore, + private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, @@ -42,6 +46,8 @@ internal class MultiWalletContentLoader( private val getStoryContentUseCase: GetStoryContentUseCase, private val swapFeatureToggles: SwapFeatureToggles, private val deepLinksRegistry: DeepLinksRegistry, + private val nftFeatureToggles: NFTFeatureToggles, + private val walletsRepository: WalletsRepository, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -58,6 +64,15 @@ internal class MultiWalletContentLoader( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, deepLinksRegistry = deepLinksRegistry, ).let(::add) + if (nftFeatureToggles.isNFTEnabled) { + WalletNFTListSubscriber( + userWallet = userWallet, + getNFTCollectionsUseCase = getNFTCollectionsUseCase, + stateHolder = stateHolder, + walletsRepository = walletsRepository, + clickIntents = clickIntents, + ).let(::add) + } MultiWalletWarningsSubscriber( userWallet = userWallet, stateHolder = stateHolder, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 7bc706075d..f5cb299df3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -3,10 +3,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -16,6 +18,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.features.nft.NFTFeatureToggles import com.tangem.features.swap.SwapFeatureToggles import javax.inject.Inject @@ -36,6 +39,9 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val getStoryContentUseCase: GetStoryContentUseCase, private val swapFeatureToggles: SwapFeatureToggles, private val deepLinksRegistry: DeepLinksRegistry, + private val nftFeatureToggles: NFTFeatureToggles, + private val walletsRepository: WalletsRepository, + private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader { @@ -56,6 +62,9 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, swapFeatureToggles = swapFeatureToggles, deepLinksRegistry = deepLinksRegistry, + nftFeatureToggles = nftFeatureToggles, + walletsRepository = walletsRepository, + getNFTCollectionsUseCase = getNFTCollectionsUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index cf9c56b44c..e9771676b7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.event.consumedEvent import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 786c3ebb5c..cd4ed81df9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -22,6 +22,7 @@ internal sealed interface WalletState : WalletStateHolder { sealed class MultiCurrency : WalletState { abstract val tokensListState: WalletTokensListState + abstract val nftState: WalletNFTItemUM data class Content( override val pullToRefreshConfig: PullToRefreshConfig, @@ -30,6 +31,7 @@ internal sealed interface WalletState : WalletStateHolder { override val warnings: ImmutableList, override val bottomSheetConfig: TangemBottomSheetConfig?, override val tokensListState: WalletTokensListState, + override val nftState: WalletNFTItemUM, ) : MultiCurrency() data class Locked( @@ -46,6 +48,7 @@ internal sealed interface WalletState : WalletStateHolder { ) { override val tokensListState = WalletTokensListState.ContentState.Locked + override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index ac0c59bc42..b22b7ea772 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -38,7 +38,9 @@ internal class InitializeWalletsTransformer( if (userWallet.isLocked) { createLockedState(userWallet) } else { - walletLoadingStateFactory.create(userWallet) + walletLoadingStateFactory.create( + userWallet = userWallet, + ) } } .toImmutableList(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index ceecfc9cfc..02d33a5f4c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.toImmutableList @@ -38,7 +38,11 @@ internal class ReinitializeWalletTransformer( return prevState.copy( wallets = prevState.wallets .filterNot { it.walletCardState.id == prevWalletId } - .plus(element = walletLoadingStateFactory.create(newUserWallet)) + .plus( + element = walletLoadingStateFactory.create( + userWallet = newUserWallet, + ), + ) .toImmutableList(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt new file mode 100644 index 0000000000..b66ed6ba9a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState + +internal class RemoveNFTCollectionsTransformer( + userWalletId: UserWalletId, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState = when (prevState) { + is WalletState.MultiCurrency.Content -> prevState.copy( + nftState = WalletNFTItemUM.Hidden, + ) + is WalletState.SingleCurrency.Content, + is WalletState.Visa.Content, + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + is WalletState.Visa.Locked, + is WalletState.Visa.AccessTokenLocked, + -> prevState + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt new file mode 100644 index 0000000000..f713b64077 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt @@ -0,0 +1,95 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.nft.models.NFTCollections +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import kotlinx.collections.immutable.toPersistentList + +internal class SetNFTCollectionsTransformer( + userWalletId: UserWalletId, + private val nftCollections: List, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState = when (prevState) { + is WalletState.MultiCurrency.Content -> prevState.copy( + nftState = when { + allCollectionsFailed() -> WalletNFTItemUM.Failed + anyCollectionFailed() && allLoadedCollectionsEmpty() -> WalletNFTItemUM.Failed + allCollectionsLoaded() && allCollectionsEmpty() -> WalletNFTItemUM.Empty + !allCollectionsLoaded() && allCollectionsEmpty() -> WalletNFTItemUM.Loading + else -> createContentNFTItemUM() + }, + ) + is WalletState.SingleCurrency.Content, + is WalletState.Visa.Content, + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + is WalletState.Visa.Locked, + is WalletState.Visa.AccessTokenLocked, + -> prevState + } + + private fun createContentNFTItemUM(): WalletNFTItemUM.Content { + val collectionsContent = nftCollections + .map { it.content } + .filterIsInstance() + + val isFlickering = collectionsContent + .map { it.source } + .any { it == StatusSource.CACHE } + + val collections = collectionsContent + .mapNotNull { it.collections } + .flatten() + + return WalletNFTItemUM.Content( + previews = if (collections.size > NFT_COLLECTIONS_MAX_PREVIEWS_COUNT) { + collections + .take(NFT_COLLECTIONS_MAX_PREVIEWS_COUNT - 1) + .map { WalletNFTItemUM.Content.CollectionPreview.Image(it.logoUrl.orEmpty()) } + .plus(WalletNFTItemUM.Content.CollectionPreview.More) + .toPersistentList() + } else { + collections + .take(NFT_COLLECTIONS_MAX_PREVIEWS_COUNT) + .map { WalletNFTItemUM.Content.CollectionPreview.Image(it.logoUrl.orEmpty()) } + .toPersistentList() + }, + collectionsCount = collections.size, + assetsCount = collections + .sumOf { it.count }, + isFlickering = isFlickering, + ) + } + + private fun allCollectionsFailed() = nftCollections.all { + it.content is NFTCollections.Content.Error + } + + private fun anyCollectionFailed() = nftCollections.any { + it.content is NFTCollections.Content.Error + } + + private fun allLoadedCollectionsEmpty() = nftCollections + .map { it.content } + .filterIsInstance() + .all { it.collections.isNullOrEmpty() } + + private fun allCollectionsLoaded() = nftCollections.all { + val content = it.content + content is NFTCollections.Content.Collections && + content.source != StatusSource.CACHE + } + + private fun allCollectionsEmpty() = nftCollections.all { + val content = it.content + content is NFTCollections.Content.Collections && + content.collections.isNullOrEmpty() + } + + companion object { + private const val NFT_COLLECTIONS_MAX_PREVIEWS_COUNT = 4 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index 61cb2c07b4..847bf806a1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -46,7 +46,9 @@ internal class UnlockWalletTransformer( is WalletState.MultiCurrency.Locked, is WalletState.SingleCurrency.Locked, is WalletState.Visa.Locked, - -> walletLoadingStateFactory.create(userWallet = unlockedWallet) + -> walletLoadingStateFactory.create( + userWallet = unlockedWallet, + ) is WalletState.MultiCurrency.Content, is WalletState.SingleCurrency.Content, is WalletState.Visa.Content, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 599932a80e..562cbd95c1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -1,14 +1,14 @@ package com.tangem.feature.wallet.presentation.wallet.state.utils +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.* -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -41,6 +41,7 @@ internal class WalletLoadingStateFactory( warnings = persistentListOf(), bottomSheetConfig = null, tokensListState = WalletTokensListState.ContentState.Loading, + nftState = WalletNFTItemUM.Hidden, ) } @@ -80,7 +81,10 @@ internal class WalletLoadingStateFactory( } private fun createPullToRefreshConfig(): PullToRefreshConfig { - return PullToRefreshConfig(onRefresh = { clickIntents.onRefreshSwipe(it.value) }, isRefreshing = false) + return PullToRefreshConfig( + onRefresh = { clickIntents.onRefreshSwipe(it.value) }, + isRefreshing = false, + ) } private fun UserWallet.toLoadingWalletCardState(): WalletCardState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt new file mode 100644 index 0000000000..476770ce55 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt @@ -0,0 +1,50 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.nft.GetNFTCollectionsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.transformers.RemoveNFTCollectionsTransformer +import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetNFTCollectionsTransformer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* + +@Suppress("UnusedPrivateMember") +internal class WalletNFTListSubscriber( + private val userWallet: UserWallet, + private val stateHolder: WalletStateController, + private val walletsRepository: WalletsRepository, + private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, + clickIntents: WalletClickIntents, +) : WalletSubscriber() { + + @OptIn(ExperimentalCoroutinesApi::class) + override fun create(coroutineScope: CoroutineScope): Flow<*> = walletsRepository + .nftEnabledStatus(userWallet.walletId) + .distinctUntilChanged() + .flatMapLatest { nftEnabled -> + // if NFT is enabled for this wallet, then start observing changes from store and apply transformer if need + if (nftEnabled) { + getNFTCollectionsUseCase + .launch(userWallet.walletId) + .shareIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) + .onEach { + stateHolder.update( + SetNFTCollectionsTransformer(userWallet.walletId, it), + ) + } + } else { + // otherwise, hide NFT from wallet + stateHolder.update( + RemoveNFTCollectionsTransformer(userWallet.walletId), + ) + emptyFlow() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 3a8e681611..ff7362b3c2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -80,6 +80,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsB import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* import com.tangem.feature.wallet.presentation.wallet.ui.components.common.actions +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.nftCollections import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.BalancesAndLimitsBottomSheet @@ -242,6 +243,8 @@ private fun WalletContent( modifier = movableItemModifier, ) + nftCollections(state = selectedWallet, itemModifier = itemModifier) + organizeTokens(state = selectedWallet, itemModifier = itemModifier) } @@ -700,6 +703,16 @@ internal fun LazyListScope.organizeTokens(state: WalletState, itemModifier: Modi } } +internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modifier) { + (state as? WalletState.MultiCurrency)?.let { + nftCollections( + modifier = itemModifier, + state = it.nftState, + onClick = {}, + ) + } +} + @Composable private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { if (bottomSheetConfig != null) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt index d31a09e214..850b0d78ab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt @@ -131,7 +131,20 @@ private fun WalletNFTItemFailed(modifier: Modifier = Modifier) { RowContentContainer( modifier = modifier, icon = { - CollectionsPreviewsPlaceholder() + Box( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) + .background(TangemTheme.colors.field.primary), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size20), + painter = painterResource(R.drawable.ic_error_sync_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } }, text = { Text( @@ -158,7 +171,12 @@ private fun WalletNFTItemLoading(modifier: Modifier = Modifier) { RowContentContainer( modifier = modifier, icon = { - CollectionsPreviewsPlaceholder() + Box( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) + .background(TangemTheme.colors.field.primary), + ) }, text = { TextShimmer( @@ -178,16 +196,6 @@ private fun WalletNFTItemLoading(modifier: Modifier = Modifier) { ) } -@Composable -private fun CollectionsPreviewsPlaceholder(modifier: Modifier = Modifier) { - Box( - modifier = modifier - .size(TangemTheme.dimens.size36) - .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) - .background(TangemTheme.colors.field.primary), - ) -} - @Composable @Suppress("MagicNumber") private fun BoxScope.CollectionsPreviews(previews: ImmutableList, modifier: Modifier = Modifier) { @@ -207,7 +215,7 @@ private fun BoxScope.CollectionsPreviews(previews: ImmutableList { SubcomposeAsyncImage( modifier = modifier, - model = s, + model = s.url, loading = { RectangleShimmer(radius = 0.dp) }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt new file mode 100644 index 0000000000..da4ce8f1d5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM +import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletNFTItem + +private const val NFT_COLLECTIONS_CONTENT_TYPE = "NFTCollections" + +internal fun LazyListScope.nftCollections(state: WalletNFTItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + item(key = NFT_COLLECTIONS_CONTENT_TYPE, contentType = NFT_COLLECTIONS_CONTENT_TYPE) { + WalletNFTItem( + modifier = modifier, + state = state, + onClick = onClick, + ) + } +} \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index e71e1ff89f..4351692f4c 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,5 +1,5 @@ [versions] -tangemBlockchainSdk = "develop-977" +tangemBlockchainSdk = "develop-984" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-441" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^