Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-18 15:59:22 +05:00
parent 5de3e403f0
commit fb2f23c8a9
35 changed files with 674 additions and 191 deletions

View file

@ -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,
)
}
}

View file

@ -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,
)
}
}

View file

@ -12,7 +12,7 @@ internal class DefaultNFTPersistenceStore(
private val pricesPersistenceStore: DataStore<Map<NFTAsset.Identifier, NFTAsset.SalePrice>>,
) : NFTPersistenceStore {
override fun getCollections(): Flow<List<NFTCollection>> = collectionsPersistenceStore.data
override fun getCollections(): Flow<List<NFTCollection>?> = collectionsPersistenceStore.data
override suspend fun getCollectionsSync(): List<NFTCollection>? = collectionsPersistenceStore
.data

View file

@ -5,7 +5,7 @@ import com.tangem.blockchain.nft.models.NFTCollection
import kotlinx.coroutines.flow.Flow
interface NFTPersistenceStore {
fun getCollections(): Flow<List<NFTCollection>>
fun getCollections(): Flow<List<NFTCollection>?>
suspend fun getCollectionsSync(): List<NFTCollection>?

View file

@ -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<NFTCollection>(),
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<NFTCollection>(),
defaultValue = emptyList(),
),
pricesPersistenceStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithCustomKeyTypes<NFTAsset.Identifier, NFTAsset.SalePrice>(),
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<NFTAsset.Identifier, NFTAsset.SalePrice>(),
defaultValue = emptyMap(),
),
)
}
private fun <T> createPersistenceStore(fileName: String, types: ParameterizedType, defaultValue: T): DataStore<T> =
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()
}

View file

@ -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<Pair<Network, SdkNFTAsset>, NFTAsset> {
object NFTSdkAssetConverter : Converter<Pair<Network, SdkNFTAsset>, NFTAsset> {
override fun convert(value: Pair<Network, SdkNFTAsset>): 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,

View file

@ -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<Pair<Network, SdkNFTCollection>, NFTCollection> {
object NFTSdkCollectionConverter : Converter<Pair<Network, SdkNFTCollection>, NFTCollection> {
override fun convert(value: Pair<Network, SdkNFTCollection>): 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

View file

@ -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<Network, JobHolder>()
private val nftRuntimeStores = mutableMapOf<Network, NFTRuntimeStore>()
private val nftPersistenceStores = mutableMapOf<Network, NFTPersistenceStore>()
private val nftSdkAssetConverter = NFTSdkAssetConverter(
nftSdkAssetIdentifierConverter = NFTSdkAssetIdentifierConverter,
nftSdkCollectionIdentifierConverter = NFTSdkCollectionIdentifierConverter,
)
private val collectionConverter = NFTSdkCollectionConverter(
nftSdkCollectionIdentifierConverter = NFTSdkCollectionIdentifierConverter,
nftSdkAssetConverter = nftSdkAssetConverter,
)
private val nftRuntimeStores = mutableMapOf<String, NFTRuntimeStore>()
private val nftPersistenceStores = mutableMapOf<String, NFTPersistenceStore>()
override fun observeCollections(userWalletId: UserWalletId, networks: List<Network>): Flow<List<NFTCollections>> =
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<Network>,
): Flow<List<NFTCollections>> = combine(
networks.map { observeCollectionsInternal(userWalletId, it) },
) { it.asList() }
private suspend fun observeCollectionsInternal(
userWalletId: UserWalletId,
network: Network,
): Flow<NFTCollections> = if (network.canHandleNFTs()) {
getNFTRuntimeStore(userWalletId, network).getCollections()
} else {
flowOf(NFTCollections.empty(network))
}
override suspend fun refreshCollections(userWalletId: UserWalletId, networks: List<Network>) = 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<SdkNFTCollection>) {
getNFTRuntimeStore(network).saveCollections(
private suspend fun saveCollectionsInRuntime(
userWalletId: UserWalletId,
network: Network,
collections: List<SdkNFTCollection>,
) {
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<SdkNFTCollection>) {
getNFTPersistenceStore(network).saveCollections(collections)
private suspend fun saveCollectionsInPersistence(
userWalletId: UserWalletId,
network: Network,
collections: List<SdkNFTCollection>,
) {
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<SdkNFTCollection>.mergeWithStoredAssets(network: Network): List<SdkNFTCollection> {
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<SdkNFTCollection>.mergeWithStoredAssets(
userWalletId: UserWalletId,
network: Network,
): List<SdkNFTCollection> {
val storedCollections =
getNFTPersistenceStore(network)
getNFTPersistenceStore(userWalletId, network)
.getCollectionsSync()
.orEmpty()
.associateBy(SdkNFTCollection::identifier)
@ -224,4 +257,11 @@ internal class DefaultNFTRepository @Inject constructor(
}
}
}
private fun Pair<UserWalletId, Network>.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
}

View file

@ -208,6 +208,10 @@ internal class DefaultWalletsRepository(
.getObjectMap<Boolean>(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY)
.map { it[userWalletId.stringValue] == true }
override fun nftEnabledStatuses(): Flow<Map<UserWalletId, Boolean>> = appPreferencesStore
.getObjectMap<Boolean>(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY)
.map { it.mapKeys { UserWalletId(it.key) } }
override suspend fun enableNFT(userWalletId: UserWalletId) {
appPreferencesStore.editData {
it.setObjectMap(

View file

@ -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,
),
)
}
}

View file

@ -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())
}
}

View file

@ -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<List<NFTCollections>> = currenciesRepository
.getWalletCurrenciesUpdates(userWalletId)
.flatMapLatest {
val networks = it
.map { cryptoCurrency -> cryptoCurrency.network }
.distinct()
nftRepository.observeCollections(userWalletId, networks)
}
}

View file

@ -32,6 +32,8 @@ interface WalletsRepository {
fun nftEnabledStatus(userWalletId: UserWalletId): Flow<Boolean>
fun nftEnabledStatuses(): Flow<Map<UserWalletId, Boolean>>
suspend fun enableNFT(userWalletId: UserWalletId)
suspend fun disableNFT(userWalletId: UserWalletId)

View file

@ -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)

View file

@ -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)

View file

@ -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<Unit>()
@ -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)

View file

@ -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})"

View file

@ -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()

View file

@ -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,
),
)
}

View file

@ -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
}
}

View file

@ -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<WalletSubscriber> {
@ -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,

View file

@ -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,
)
}
}

View file

@ -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

View file

@ -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<WalletNotification>,
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
}
}

View file

@ -38,7 +38,9 @@ internal class InitializeWalletsTransformer(
if (userWallet.isLocked) {
createLockedState(userWallet)
} else {
walletLoadingStateFactory.create(userWallet)
walletLoadingStateFactory.create(
userWallet = userWallet,
)
}
}
.toImmutableList(),

View file

@ -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(),
)
}

View file

@ -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
}
}

View file

@ -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<NFTCollections>,
) : 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<NFTCollections.Content.Collections>()
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<NFTCollections.Content.Collections>()
.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
}
}

View file

@ -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,

View file

@ -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 {

View file

@ -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()
}
}
}

View file

@ -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) {

View file

@ -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<CollectionPreview>, modifier: Modifier = Modifier) {
@ -207,7 +215,7 @@ private fun BoxScope.CollectionsPreviews(previews: ImmutableList<CollectionPrevi
is CollectionPreview.Image -> {
SubcomposeAsyncImage(
modifier = modifier,
model = s,
model = s.url,
loading = {
RectangleShimmer(radius = 0.dp)
},

View file

@ -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,
)
}
}

View file

@ -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 ^