Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-15 23:47:29 +05:00
parent 4cd56a1588
commit 17ae97606e
13 changed files with 119 additions and 64 deletions

View file

@ -23,6 +23,7 @@ dependencies {
/** Project */
implementation(projects.core.analytics)
implementation(projects.core.utils)
implementation(projects.core.res)
implementation(projects.libs.auth)
implementation(projects.domain.appTheme.models)
implementation(projects.domain.core)

View file

@ -122,7 +122,7 @@ internal class DefaultNFTRuntimeStore(
)
}
?.filter { it.count > 0 }
?.sortedBy { it.name },
?.sortedBy { it.name?.lowercase() },
source = this.source,
)

View file

@ -1,5 +1,7 @@
package com.tangem.datasource.local.nft.converter
import android.content.res.Resources
import com.tangem.datasource.R
import com.tangem.domain.models.StatusSource
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
@ -7,14 +9,31 @@ import com.tangem.domain.tokens.model.Network
import com.tangem.utils.converter.Converter
import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection
object NFTSdkCollectionConverter : Converter<Pair<Network, SdkNFTCollection>, NFTCollection> {
class NFTSdkCollectionConverter(
private val resources: Resources,
) : Converter<Pair<Network, SdkNFTCollection>, NFTCollection> {
override fun convert(value: Pair<Network, SdkNFTCollection>): NFTCollection {
val (network, collection) = value
val collectionId = NFTSdkCollectionIdentifierConverter.convert(collection.identifier)
return NFTCollection(
id = collectionId,
network = network,
name = collection.name,
// We use localised strings from resources here to proceed sorting and searching correctly
// Sorting is invoked on data layer, searching is simplified to filtering for now and invoked in Model
name = when (collectionId) {
is NFTCollection.Identifier.EVM -> collection.name
is NFTCollection.Identifier.TON -> when {
collectionId.contractAddress == null -> resources.getString(R.string.nft_no_collection)
collection.name.isNullOrEmpty() -> resources.getString(R.string.nft_untitled_collection)
else -> collection.name
}
is NFTCollection.Identifier.Solana -> when {
collectionId.collectionAddress == null -> resources.getString(R.string.nft_no_collection)
collection.name.isNullOrEmpty() -> resources.getString(R.string.nft_untitled_collection)
else -> collection.name
}
NFTCollection.Identifier.Unknown -> null
},
description = collection.description,
logoUrl = collection.logoUrl,
count = collection.count,

View file

@ -13,7 +13,7 @@ object NFTSdkCollectionIdentifierConverter : TwoWayConverter<SdkNFTCollection.Id
contractAddress = value.contractAddress,
)
is SdkNFTCollection.Identifier.Solana -> NFTCollection.Identifier.Solana(
collection = value.collection,
collectionAddress = value.collectionAddress,
)
is SdkNFTCollection.Identifier.Unknown -> NFTCollection.Identifier.Unknown
}
@ -26,7 +26,7 @@ object NFTSdkCollectionIdentifierConverter : TwoWayConverter<SdkNFTCollection.Id
contractAddress = value.contractAddress,
)
is NFTCollection.Identifier.Solana -> SdkNFTCollection.Identifier.Solana(
collection = value.collection,
collectionAddress = value.collectionAddress,
)
is NFTCollection.Identifier.Unknown -> SdkNFTCollection.Identifier.Unknown
}

View file

@ -49,6 +49,6 @@ dependencies {
implementation(tangemDeps.card.core)
/** DI */
implementation(deps.hilt.core)
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -1,5 +1,6 @@
package com.tangem.data.nft
import android.content.res.Resources
import arrow.core.Either
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
@ -48,6 +49,7 @@ internal class DefaultNFTRepository @Inject constructor(
private val excludedBlockchains: ExcludedBlockchains,
private val userWalletsStore: UserWalletsStore,
private val nftFeatureToggles: NFTFeatureToggles,
resources: Resources,
) : NFTRepository {
private val networkJobs = ConcurrentHashMap<Network, JobHolder>()
@ -59,6 +61,7 @@ internal class DefaultNFTRepository @Inject constructor(
private val collectionIdConverter = NFTSdkCollectionIdentifierConverter
private val assetIdConverter = NFTSdkAssetIdentifierConverter
private val nftSdkCollectionConverter by lazy { NFTSdkCollectionConverter(resources) }
override fun observeCollections(userWalletId: UserWalletId, networks: List<Network>): Flow<List<NFTCollections>> =
flow { emitAll(observeCollectionsInternal(userWalletId, networks)) }
@ -319,7 +322,7 @@ internal class DefaultNFTRepository @Inject constructor(
content = NFTCollections.Content.Collections(
collections = collections
.map { collection ->
NFTSdkCollectionConverter.convert(network to collection)
nftSdkCollectionConverter.convert(network to collection)
}
.filter {
it.id !is NFTCollection.Identifier.Unknown
@ -417,7 +420,7 @@ internal class DefaultNFTRepository @Inject constructor(
content = NFTCollections.Content.Collections(
collections = it
?.map { collection ->
NFTSdkCollectionConverter.convert(network to collection)
nftSdkCollectionConverter.convert(network to collection)
}
?.filter {
it.id !is NFTCollection.Identifier.Unknown

View file

@ -1,18 +1,45 @@
package com.tangem.data.nft.di
import android.content.Context
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.nft.DefaultNFTRepository
import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory
import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.nft.repository.NFTRepository
import dagger.Binds
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.features.nft.NFTFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface NFTDataModule {
internal object NFTDataModule {
@Binds
@Provides
@Singleton
fun bindNFTRepository(repository: DefaultNFTRepository): NFTRepository
fun provideNFTRepository(
@ApplicationContext context: Context,
nftPersistenceStoreFactory: NFTPersistenceStoreFactory,
nftRuntimeStoreFactory: NFTRuntimeStoreFactory,
walletManagersFacade: WalletManagersFacade,
dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
userWalletsStore: UserWalletsStore,
nftFeatureToggles: NFTFeatureToggles,
): NFTRepository = DefaultNFTRepository(
nftPersistenceStoreFactory = nftPersistenceStoreFactory,
nftRuntimeStoreFactory = nftRuntimeStoreFactory,
walletManagersFacade = walletManagersFacade,
dispatchers = dispatchers,
excludedBlockchains = excludedBlockchains,
userWalletsStore = userWalletsStore,
nftFeatureToggles = nftFeatureToggles,
resources = context.resources,
)
}

View file

@ -41,7 +41,7 @@ data class NFTCollection(
data class TON(val contractAddress: String?) : Identifier()
@Serializable
data class Solana(val collection: String?) : Identifier()
data class Solana(val collectionAddress: String?) : Identifier()
@Serializable
data object Unknown : Identifier()

View file

@ -5,7 +5,7 @@ import com.tangem.core.ui.extensions.TextReference
internal data class NFTCollectionUM(
val id: String,
val name: String?,
val name: String,
@DrawableRes val networkIconId: Int,
val logoUrl: String?,
val description: TextReference,

View file

@ -15,7 +15,6 @@ import kotlinx.collections.immutable.toPersistentList
@Suppress("LongParameterList")
internal class UpdateDataStateTransformer(
private val nftCollections: List<NFTCollections>,
private val searchQuery: String,
private val onReceiveClick: () -> Unit,
private val onRetryClick: () -> Unit,
private val onExpandCollectionClick: (NFTCollection) -> Unit,
@ -55,7 +54,7 @@ internal class UpdateDataStateTransformer(
.map { it.content }
.asSequence()
.filterIsInstance<NFTCollections.Content.Collections>()
.map { it.collections.orEmpty().transform(prevState, searchQuery) }
.map { it.collections.orEmpty().transform(prevState) }
.flatten()
.toPersistentList(),
warnings = transformNotifications(),
@ -74,48 +73,23 @@ internal class UpdateDataStateTransformer(
)
}
private fun List<NFTCollection>.transform(
state: NFTCollectionsStateUM,
query: String,
): ImmutableList<NFTCollectionUM> = mapNotNull {
val assetsFulfillQuery = if (query.isEmpty()) {
true
} else {
when (val assets = it.assets) {
is NFTCollection.Assets.Empty,
is NFTCollection.Assets.Failed,
is NFTCollection.Assets.Loading,
-> false
is NFTCollection.Assets.Value -> {
assets.items.any { asset ->
asset.name?.lowercase()?.contains(query.lowercase()) == true
}
}
}
}
val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true
if (collectionFulfillQuery || assetsFulfillQuery) {
NFTCollectionUM(
id = it.collectionIdProvider(),
networkIconId = getActiveIconRes(it.network.id.value),
name = it.name,
description = TextReference.PluralRes(
R.plurals.nft_collections_count,
it.count,
wrappedList(it.count),
),
logoUrl = it.logoUrl,
assets = it.transformAssets(),
onExpandClick = {
onExpandCollectionClick(it)
},
isExpanded = it.isExpanded(state),
)
} else {
null
}
private fun List<NFTCollection>.transform(state: NFTCollectionsStateUM): ImmutableList<NFTCollectionUM> = map {
NFTCollectionUM(
id = it.collectionIdProvider(),
networkIconId = getActiveIconRes(it.network.id.value),
name = it.name.orEmpty(),
description = TextReference.PluralRes(
R.plurals.nft_collections_count,
it.count,
wrappedList(it.count),
),
logoUrl = it.logoUrl,
assets = it.transformAssets(),
onExpandClick = {
onExpandCollectionClick(it)
},
isExpanded = it.isExpanded(state),
)
}.toPersistentList()
private fun transformNotifications(): ImmutableList<NFTCollectionsWarningUM> = buildList {

View file

@ -11,6 +11,7 @@ import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase
import com.tangem.domain.nft.GetNFTCollectionsUseCase
import com.tangem.domain.nft.RefreshAllNFTUseCase
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.nft.models.NFTCollections
import com.tangem.features.nft.collections.NFTCollectionsComponent
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
@ -71,8 +72,7 @@ internal class NFTCollectionsModel @Inject constructor(
) { nftCollections, query ->
_state.update {
UpdateDataStateTransformer(
nftCollections = nftCollections,
searchQuery = query,
nftCollections = nftCollections.filter(query),
onReceiveClick = {
params.onReceiveClick()
},
@ -91,6 +91,38 @@ internal class NFTCollectionsModel @Inject constructor(
.launchIn(modelScope)
}
private fun List<NFTCollections>.filter(query: String): List<NFTCollections> = map {
it.copy(
content = when (val content = it.content) {
is NFTCollections.Content.Collections -> content.copy(
collections = content.collections.orEmpty().filter {
val assetsFulfillQuery = if (query.isEmpty()) {
true
} else {
when (val assets = it.assets) {
is NFTCollection.Assets.Empty,
is NFTCollection.Assets.Failed,
is NFTCollection.Assets.Loading,
-> false
is NFTCollection.Assets.Value -> {
assets.items.any { asset ->
asset.name?.lowercase()?.contains(query.lowercase()) == true
}
}
}
}
val collectionFulfillQuery =
query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true
collectionFulfillQuery || assetsFulfillQuery
},
)
is NFTCollections.Content.Error -> it.content
},
)
}
private fun onRefresh() {
modelScope.launch {
_state.update { ChangeRefreshingStateTransformer(true).transform(it) }

View file

@ -18,7 +18,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.collections.entity.NFTCollectionAssetsListUM
@ -90,7 +89,7 @@ private fun RowScope.Text(state: NFTCollectionUM) {
) {
Text(
modifier = Modifier,
text = state.name.takeUnless { it.isNullOrEmpty() } ?: stringResourceSafe(R.string.nft_no_collection),
text = state.name,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
maxLines = 1,

View file

@ -5,7 +5,7 @@
# https://github.com/tangem/tangem-sdk-android/
# https://github.com/tangem/vico
tangemBlockchainSdk = "develop-1063"
tangemBlockchainSdk = "develop-1064"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-468"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
@ -21,4 +21,4 @@ card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tange
vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" }
vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" }
vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" }
vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" }