Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-05 03:49:15 +07:00
parent 7fee3c6d3b
commit 70dd41a680
19 changed files with 594 additions and 184 deletions

View file

@ -13,6 +13,7 @@ android {
dependencies {
/** Api */
implementation(projects.features.account.api)
implementation(projects.features.nft.api)
implementation(projects.features.tokenRecieve.api)
@ -28,6 +29,8 @@ dependencies {
implementation(projects.core.datasource)
/** Domain modules */
implementation(projects.domain.account)
implementation(projects.domain.wallets)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.appCurrency)
implementation(projects.domain.models)

View file

@ -1,10 +1,20 @@
package com.tangem.features.nft.collections.entity
import androidx.annotation.DrawableRes
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.core.ui.extensions.TextReference
internal sealed interface NFTCollectionItem {
val id: String
}
internal data class NFTCollectionPortfolioUM(
override val id: String,
val title: AccountTitleUM,
) : NFTCollectionItem
internal data class NFTCollectionUM(
val id: String,
override val id: String,
val name: String,
@DrawableRes val networkIconId: Int,
val logoUrl: String?,
@ -12,4 +22,4 @@ internal data class NFTCollectionUM(
val assets: NFTCollectionAssetsListUM,
val isExpanded: Boolean,
val onExpandClick: () -> Unit,
)
) : NFTCollectionItem

View file

@ -23,7 +23,7 @@ internal sealed class NFTCollectionsUM {
data class Content(
val search: SearchBarUM,
val collections: ImmutableList<NFTCollectionUM>,
val collections: ImmutableList<NFTCollectionItem>,
val warnings: ImmutableList<NFTCollectionsWarningUM>,
val onReceiveClick: () -> Unit,
) : NFTCollectionsUM()

View file

@ -1,6 +1,7 @@
package com.tangem.features.nft.collections.entity.transformer
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.features.nft.collections.entity.NFTCollectionUM
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
import com.tangem.utils.transformer.Transformer
@ -21,7 +22,7 @@ internal class ChangeCollectionExpandedStateTransformer(
is NFTCollectionsUM.Content -> prevState.content.copy(
collections = prevState.content.collections.map {
val collectionId = collection.collectionIdProvider()
if (it.id == collectionId) {
if (it.id == collectionId && it is NFTCollectionUM) {
if (!it.isExpanded) {
onFirstExpanded()
}

View file

@ -1,10 +1,13 @@
package com.tangem.features.nft.collections.entity.transformer
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.models.account.Account
import com.tangem.domain.nft.models.*
import com.tangem.features.nft.collections.entity.*
import com.tangem.features.nft.impl.R
@ -16,6 +19,8 @@ import kotlinx.collections.immutable.toPersistentList
@Suppress("LongParameterList")
internal class UpdateDataStateTransformer(
private val nftCollections: List<NFTCollections>,
private val walletNFTCollections: WalletNFTCollections? = null,
private val isAccountMode: Boolean = false,
private val onReceiveClick: () -> Unit,
private val onRetryClick: () -> Unit,
private val onExpandCollectionClick: (NFTCollection) -> Unit,
@ -25,7 +30,9 @@ internal class UpdateDataStateTransformer(
private val collectionIdProvider: NFTCollection.() -> String,
) : Transformer<NFTCollectionsStateUM> {
@Suppress("CyclomaticComplexMethod")
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM {
val nftCollections = walletNFTCollections?.flattenCollections ?: this.nftCollections
val hasQuery = !(prevState.content as? NFTCollectionsUM.Content)?.search?.query.isNullOrEmpty()
val content = when {
!hasQuery && nftCollections.allCollectionsFailed() ->
@ -66,16 +73,45 @@ internal class UpdateDataStateTransformer(
} else {
initialSearchBarFactory()
},
collections = nftCollections
collections = walletNFTCollections
?.let { createCollections(it) }
?: createNFTsUM(nftCollections).toPersistentList(),
warnings = transformNotifications(),
onReceiveClick = onReceiveClick,
)
private fun NFTCollectionsStateUM.createCollections(walletNFTCollections: WalletNFTCollections) =
if (isAccountMode) {
val result = mutableListOf<NFTCollectionItem>()
walletNFTCollections.collections.forEach { (account, nfts) ->
if (nfts.isEmpty()) return@forEach
result.add(account.toAccountPortfolioUM())
result.addAll(createNFTsUM(nfts))
}
result.toPersistentList()
} else {
val mainAccountCollection = walletNFTCollections.collections.values.firstOrNull() ?: listOf()
createNFTsUM(mainAccountCollection).toPersistentList()
}
private fun Account.toAccountPortfolioUM(): NFTCollectionPortfolioUM = NFTCollectionPortfolioUM(
id = this.accountId.value,
title = AccountTitleUM.Account(
prefixText = TextReference.EMPTY,
name = this.accountName.toUM().value,
icon = when (this) {
is Account.CryptoPortfolio -> this.icon.toUM()
},
),
)
private fun NFTCollectionsStateUM.createNFTsUM(nftCollections: List<NFTCollections>): Sequence<NFTCollectionUM> =
nftCollections
.map { it.content }
.asSequence()
.filterIsInstance<NFTCollections.Content.Collections>()
.map { it.collections.orEmpty().transform(this) }
.flatten()
.toPersistentList(),
warnings = transformNotifications(),
onReceiveClick = onReceiveClick,
)
private fun List<NFTCollection>.transform(state: NFTCollectionsStateUM): ImmutableList<NFTCollectionUM> = map {
NFTCollectionUM(
@ -97,6 +133,8 @@ internal class UpdateDataStateTransformer(
}.toPersistentList()
private fun transformNotifications(): ImmutableList<NFTCollectionsWarningUM> = buildList {
val nftCollections = walletNFTCollections?.flattenCollections
?: this@UpdateDataStateTransformer.nftCollections
if (nftCollections.anyCollectionFailed()) {
add(
NFTCollectionsWarningUM(
@ -152,6 +190,7 @@ internal class UpdateDataStateTransformer(
private fun NFTCollection.isExpanded(state: NFTCollectionsStateUM): Boolean =
(state.content as? NFTCollectionsUM.Content)
?.collections
?.filterIsInstance<NFTCollectionUM>()
?.firstOrNull { it.id == this.collectionIdProvider() }
?.isExpanded
?: false

View file

@ -7,6 +7,8 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi
import com.tangem.core.ui.components.fields.InputManager
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase
import com.tangem.domain.nft.GetNFTCollectionsUseCase
import com.tangem.domain.nft.RefreshAllNFTUseCase
@ -31,6 +33,8 @@ internal class NFTCollectionsModel @Inject constructor(
private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase,
private val fetchNFTCollectionAssetsUseCase: FetchNFTCollectionAssetsUseCase,
private val refreshAllNFTUseCase: RefreshAllNFTUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
paramsContainer: ParamsContainer,
) : Model() {
@ -62,7 +66,11 @@ internal class NFTCollectionsModel @Inject constructor(
}
init {
subscribeToNFTCollections()
if (accountsFeatureToggles.isFeatureEnabled) {
subscribeToNFTCollectionsNew()
} else {
subscribeToNFTCollections()
}
}
private fun subscribeToNFTCollections() {
@ -91,6 +99,38 @@ internal class NFTCollectionsModel @Inject constructor(
.launchIn(modelScope)
}
private fun subscribeToNFTCollectionsNew() {
combine(
flow = getNFTCollectionsUseCase.invokeForAccounts(params.userWalletId),
flow2 = searchManager.query.distinctUntilChanged(),
flow3 = isAccountsModeEnabledUseCase(),
) { nftCollections, query, isAccountMode ->
val filteredNFTs = nftCollections.collections
.mapValues { (_, nfts) -> nfts.filter(query) }
_state.update {
UpdateDataStateTransformer(
nftCollections = listOf(),
isAccountMode = isAccountMode,
walletNFTCollections = nftCollections.copy(collections = filteredNFTs),
onReceiveClick = {
params.onReceiveClick()
},
onRetryClick = ::onRefresh,
onExpandCollectionClick = ::onExpandCollectionClick,
onRetryAssetsClick = ::onRetryAssetsClick,
onAssetClick = { asset, collection ->
params.onAssetClick(asset, collection)
},
initialSearchBarFactory = ::getInitialSearchBar,
collectionIdProvider = collectionIdProvider,
).transform(it)
}
}
.onStart { onRefresh() }
.launchIn(modelScope)
}
private fun List<NFTCollections>.filter(query: String): List<NFTCollections> = map {
it.copy(
content = when (val content = it.content) {

View file

@ -8,30 +8,38 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.account.AccountTitle
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.collections.entity.*
import com.tangem.features.nft.impl.R
import kotlinx.collections.immutable.persistentListOf
import java.util.UUID
@Suppress("LongMethod")
@Composable
@ -89,41 +97,24 @@ internal fun NFTCollectionsContent(content: NFTCollectionsUM.Content, modifier:
.padding(
top = TangemTheme.dimens.spacing16,
bottom = bottomPadding,
)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.primary),
),
// .clip(TangemTheme.shapes.roundedCornersXMedium)
// .background(TangemTheme.colors.background.primary),
state = listState,
) {
content.collections.fastForEach { collection ->
item(key = collection.id) {
NFTCollection(
modifier = Modifier.fillMaxWidth(),
state = collection,
content.collections.fastForEachIndexed { index, item ->
val previousItem = content.collections.getOrNull(index.dec())
val nextItem = content.collections.getOrNull(index.inc())
when (item) {
is NFTCollectionPortfolioUM -> nftPortfolioItem(
item = item,
shape = getRoundShape(item, previousItem, nextItem),
)
is NFTCollectionUM -> nftCollectionItem(
collection = item,
shape = getRoundShape(item, previousItem, nextItem),
isNextItemNFTCollection = nextItem is NFTCollectionUM,
)
}
when (val assets = collection.assets) {
is NFTCollectionAssetsListUM.Init -> Unit
is NFTCollectionAssetsListUM.Loading -> {
assetsListLoading(
collectionId = collection.id,
content = assets,
expanded = collection.isExpanded,
)
}
is NFTCollectionAssetsListUM.Failed -> {
assetsListFailed(
collectionId = collection.id,
content = assets,
expanded = collection.isExpanded,
)
}
is NFTCollectionAssetsListUM.Content -> {
assetsListContent(
collectionId = collection.id,
content = assets,
expanded = collection.isExpanded,
)
}
}
}
}
@ -139,9 +130,94 @@ internal fun NFTCollectionsContent(content: NFTCollectionsUM.Content, modifier:
}
}
private fun getRoundShape(
item: NFTCollectionItem,
previousItem: NFTCollectionItem?,
nextItem: NFTCollectionItem?,
): RoundedCornerShape {
val radius = 16.dp
val topRound = RoundedCornerShape(topStart = radius, topEnd = radius)
val bottomRound = RoundedCornerShape(bottomStart = radius, bottomEnd = radius)
val allRound = RoundedCornerShape(size = radius)
return when (item) {
is NFTCollectionPortfolioUM -> topRound
is NFTCollectionUM -> when {
previousItem == null && nextItem == null -> allRound
previousItem == null && nextItem is NFTCollectionUM -> topRound
previousItem != null && nextItem !is NFTCollectionUM -> bottomRound
else -> RoundedCornerShape(0.dp)
}
}
}
private fun LazyListScope.nftPortfolioItem(item: NFTCollectionPortfolioUM, shape: RoundedCornerShape) {
item(item.id) {
AccountTitle(
textColor = TangemTheme.colors.text.primary1,
textStyle = TangemTheme.typography.caption1,
accountTitleUM = item.title,
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp)
.clip(shape)
.background(TangemTheme.colors.background.action)
.padding(start = 12.dp, end = 12.dp, top = 12.dp, bottom = 8.dp),
)
}
}
private fun LazyListScope.nftCollectionItem(
collection: NFTCollectionUM,
shape: RoundedCornerShape,
isNextItemNFTCollection: Boolean,
) {
item(key = collection.id) {
val itemShape = if (collection.isExpanded) {
shape.copy(bottomEnd = CornerSize(0.dp), bottomStart = CornerSize(0.dp))
} else {
shape
}
NFTCollection(
modifier = Modifier
.clip(itemShape)
.background(TangemTheme.colors.background.action)
.fillMaxWidth(),
state = collection,
)
}
when (val assets = collection.assets) {
is NFTCollectionAssetsListUM.Init -> Unit
is NFTCollectionAssetsListUM.Loading -> {
assetsListLoading(
collectionId = collection.id,
content = assets,
expanded = collection.isExpanded,
isNextItemNFTCollection = isNextItemNFTCollection,
)
}
is NFTCollectionAssetsListUM.Failed -> {
assetsListFailed(
collectionId = collection.id,
content = assets,
expanded = collection.isExpanded,
isNextItemNFTCollection = isNextItemNFTCollection,
)
}
is NFTCollectionAssetsListUM.Content -> {
assetsListContent(
collectionId = collection.id,
content = assets,
expanded = collection.isExpanded,
isNextItemNFTCollection = isNextItemNFTCollection,
)
}
}
}
private fun LazyListScope.assetsListLoading(
collectionId: String,
content: NFTCollectionAssetsListUM.Loading,
isNextItemNFTCollection: Boolean,
expanded: Boolean,
) {
val itemsCount = content.itemsCount
@ -164,6 +240,8 @@ private fun LazyListScope.assetsListLoading(
Row(
modifier = Modifier
.fillMaxWidth()
.clip(assetsShape(isNextItemNFTCollection))
.background(TangemTheme.colors.background.action)
.padding(TangemTheme.dimens.spacing6),
) {
NFTCollectionAssetLoading(
@ -193,6 +271,7 @@ private fun LazyListScope.assetsListLoading(
private fun LazyListScope.assetsListFailed(
collectionId: String,
content: NFTCollectionAssetsListUM.Failed,
isNextItemNFTCollection: Boolean,
expanded: Boolean,
) {
item(
@ -206,6 +285,8 @@ private fun LazyListScope.assetsListFailed(
Box(
modifier = Modifier
.fillMaxWidth()
.clip(assetsShape(isNextItemNFTCollection))
.background(TangemTheme.colors.background.action)
.height(TangemTheme.dimens.size142),
contentAlignment = Alignment.Center,
) {
@ -220,6 +301,7 @@ private fun LazyListScope.assetsListFailed(
private fun LazyListScope.assetsListContent(
collectionId: String,
content: NFTCollectionAssetsListUM.Content,
isNextItemNFTCollection: Boolean,
expanded: Boolean,
) {
val items = content.items
@ -244,6 +326,10 @@ private fun LazyListScope.assetsListContent(
)
Row(
modifier = Modifier
.conditional(rowIndex.inc() == rowCount) {
clip(assetsShape(isNextItemNFTCollection))
}
.background(TangemTheme.colors.background.action)
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing6),
) {
@ -277,105 +363,150 @@ private fun LazyListScope.assetsListContent(
}
}
private fun assetsShape(isNextItemNFTCollection: Boolean): Shape = if (isNextItemNFTCollection) {
RoundedCornerShape(0.dp)
} else {
RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp)
}
@Suppress("LongMethod")
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_NFTCollectionsContent() {
private fun Preview_NFTCollectionsContent(@PreviewParameter(PreviewProvider::class) state: NFTCollectionsUM.Content) {
TangemThemePreview {
NFTCollectionsContent(
content = NFTCollectionsUM.Content(
search = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = { },
content = state,
)
}
}
private class PreviewProvider : PreviewParameterProvider<NFTCollectionsUM.Content> {
val search
get() = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = { },
)
val warnings
get() = persistentListOf(
NFTCollectionsWarningUM(
id = "loading troubles",
config = NotificationConfig(
title = TextReference.Res(R.string.nft_collections_warning_title),
subtitle = TextReference.Res(R.string.nft_collections_warning_subtitle),
iconResId = R.drawable.ic_alert_triangle_20,
),
collections = persistentListOf(
NFTCollectionUM(
),
)
val collection
get() = NFTCollectionUM(
id = UUID.randomUUID().toString(),
name = "Nethers",
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Content(persistentListOf()),
isExpanded = false,
onExpandClick = { },
)
val collectionLoading
get() = collection.copy(
assets = NFTCollectionAssetsListUM.Loading(
itemsCount = 1,
),
isExpanded = true,
)
val collectionFailed
get() = collection.copy(
assets = NFTCollectionAssetsListUM.Failed(
onRetryClick = { },
),
isExpanded = true,
)
val collectionContent
get() = collection.copy(
assets = NFTCollectionAssetsListUM.Content(
items = persistentListOf(
NFTCollectionAssetUM(
id = "item1",
name = "Nethers",
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Content(persistentListOf()),
isExpanded = false,
onExpandClick = { },
name = "Nethers #0854",
imageUrl = "img",
price = NFTSalePriceUM.Content(
price = stringReference("0.05 ETH"),
),
onItemClick = { },
),
NFTCollectionUM(
NFTCollectionAssetUM(
id = "item2",
name = "Nethers",
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Loading(
itemsCount = 1,
),
isExpanded = true,
onExpandClick = { },
name = "Nethers #0855",
imageUrl = "img",
price = NFTSalePriceUM.Loading,
onItemClick = { },
),
NFTCollectionUM(
NFTCollectionAssetUM(
id = "item3",
name = "Nethers",
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Failed(
onRetryClick = { },
),
isExpanded = true,
onExpandClick = { },
),
NFTCollectionUM(
id = "item4",
name = "Nethers",
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Content(
items = persistentListOf(
NFTCollectionAssetUM(
id = "item1",
name = "Nethers #0854",
imageUrl = "img",
price = NFTSalePriceUM.Content(
price = stringReference("0.05 ETH"),
),
onItemClick = { },
),
NFTCollectionAssetUM(
id = "item2",
name = "Nethers #0855",
imageUrl = "img",
price = NFTSalePriceUM.Loading,
onItemClick = { },
),
NFTCollectionAssetUM(
id = "item3",
name = "Nethers #0856",
imageUrl = "img",
price = NFTSalePriceUM.Failed,
onItemClick = { },
),
),
),
isExpanded = true,
onExpandClick = { },
name = "Nethers #0856",
imageUrl = "img",
price = NFTSalePriceUM.Failed,
onItemClick = { },
),
),
warnings = persistentListOf(
NFTCollectionsWarningUM(
id = "loading troubles",
config = NotificationConfig(
title = TextReference.Res(R.string.nft_collections_warning_title),
subtitle = TextReference.Res(R.string.nft_collections_warning_subtitle),
iconResId = R.drawable.ic_alert_triangle_20,
),
),
),
isExpanded = true,
)
val accountHeader
get() = NFTCollectionPortfolioUM(
title = AccountTitleUM.Account(
icon = AccountIconPreviewData.randomAccountIcon(),
name = stringReference("Main Account"),
prefixText = TextReference.EMPTY,
),
id = UUID.randomUUID().toString(),
)
override val values: Sequence<NFTCollectionsUM.Content>
get() = sequenceOf(
NFTCollectionsUM.Content(
search = search,
collections = persistentListOf(
collection,
collectionLoading,
collectionFailed,
collectionContent,
),
warnings = warnings,
onReceiveClick = { },
),
NFTCollectionsUM.Content(
search = search,
collections = persistentListOf(
accountHeader,
collection,
accountHeader,
collectionContent,
collection,
),
warnings = warnings,
onReceiveClick = { },
),
NFTCollectionsUM.Content(
search = search,
collections = persistentListOf(
collectionContent,
collection,
),
warnings = warnings,
onReceiveClick = { },
),
)
}
}

View file

@ -4,7 +4,12 @@ import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
@ -13,7 +18,13 @@ import com.arkivanov.decompose.value.subscribe
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.models.PortfolioId
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.account.PortfolioSelectorController
import com.tangem.features.nft.collections.NFTCollectionsComponent
import com.tangem.features.nft.common.ui.NFTContent
import com.tangem.features.nft.component.NFTComponent
@ -23,20 +34,26 @@ import com.tangem.features.nft.entity.NFTSendSuccessListener
import com.tangem.features.nft.receive.NFTReceiveComponent
import com.tangem.features.nft.traits.NFTAssetTraitsComponent
import com.tangem.features.tokenreceive.TokenReceiveComponent
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.serialization.builtins.serializer
@Suppress("LongParameterList")
internal class DefaultNFTComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: NFTComponent.Params,
private val nftDetailsInfoComponentFactory: NFTDetailsInfoComponent.Factory,
nftSendSuccessListener: NFTSendSuccessListener,
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
private val portfolioSelectorController: PortfolioSelectorController,
portfolioFetcherFactory: PortfolioFetcher.Factory,
private val accountsFeatureToggles: AccountsFeatureToggles,
) : NFTComponent, AppComponentContext by appComponentContext {
private val stackNavigation = StackNavigation<NFTRoute>()
@ -48,6 +65,26 @@ internal class DefaultNFTComponent @AssistedInject constructor(
private val initialRoute: NFTRoute = NFTRoute.Collections(params.userWalletId)
private val currentRoute = MutableStateFlow(initialRoute)
private val onReceiveClickJob = JobHolder()
private val portfolioFetcher: PortfolioFetcher? = if (accountsFeatureToggles.isFeatureEnabled) {
portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.Wallet(params.userWalletId),
scope = componentScope,
)
} else {
null
}
private val bottomSheetNavigation: SlotNavigation<Unit> = SlotNavigation()
private val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback {
override val onDismiss: () -> Unit = { bottomSheetNavigation.dismiss() }
override val onBack: () -> Unit = { bottomSheetNavigation.dismiss() }
}
private val bottomSheetSlot = childSlot(
source = bottomSheetNavigation,
serializer = Unit.serializer(),
handleBackButton = false,
childFactory = { configuration, context -> bottomSheetChild(context) },
)
private val childStack = childStack(
key = "sendInnerStack",
@ -90,6 +127,8 @@ internal class DefaultNFTComponent @AssistedInject constructor(
NFTContent(
stackState = stackState,
)
val bottomSheet by bottomSheetSlot.subscribeAsState()
bottomSheet.child?.instance?.BottomSheet()
}
private fun createChild(route: NFTRoute, factoryContext: AppComponentContext) = when (route) {
@ -108,11 +147,15 @@ internal class DefaultNFTComponent @AssistedInject constructor(
userWalletId = route.userWalletId,
onBackClick = ::onChildBack,
onReceiveClick = {
innerRouter.push(
NFTRoute.Receive(
userWalletId = route.userWalletId,
),
)
if (accountsFeatureToggles.isFeatureEnabled) {
onReceiveClick(route)
} else {
innerRouter.push(
NFTRoute.Receive(
portfolioId = PortfolioId(route.userWalletId),
),
)
}
},
onAssetClick = { asset, collection ->
innerRouter.push(
@ -126,14 +169,31 @@ internal class DefaultNFTComponent @AssistedInject constructor(
),
)
private fun onReceiveClick(route: NFTRoute.Collections) = componentScope.launch {
val portfolioFetcher = requireNotNull(portfolioFetcher)
portfolioSelectorController.selectAccount(null)
portfolioFetcher.updateMode(mode = PortfolioFetcher.Mode.Wallet(route.userWalletId))
val portfolioData = portfolioFetcher.data.first()
if (portfolioData.isSingleChoice) {
val mainAccountId = portfolioData.balances.values.first()
.accountsBalance.mainAccount.account.accountId
innerRouter.push(NFTRoute.Receive(portfolioId = PortfolioId(mainAccountId)))
} else {
bottomSheetNavigation.activate(Unit)
val selectedAccountId = portfolioSelectorController.selectedAccount
.filterNotNull().first()
bottomSheetNavigation.dismiss()
innerRouter.push(NFTRoute.Receive(portfolioId = PortfolioId(selectedAccountId)))
}
}.saveIn(onReceiveClickJob)
private fun getReceiveComponent(
factoryContext: AppComponentContext,
route: NFTRoute.Receive,
): ComposableContentComponent = NFTReceiveComponent(
context = factoryContext,
params = NFTReceiveComponent.Params(
userWalletId = route.userWalletId,
walletName = params.walletName,
portfolioId = route.portfolioId,
onBackClick = ::onChildBack,
),
tokenReceiveComponentFactory = tokenReceiveComponentFactory,
@ -181,6 +241,16 @@ internal class DefaultNFTComponent @AssistedInject constructor(
}
}
private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent =
portfolioSelectorComponentFactory.create(
context = childByContext(componentContext),
params = PortfolioSelectorComponent.Params(
portfolioFetcher = portfolioFetcher!!,
controller = portfolioSelectorController,
bsCallback = portfolioSelectorCallback,
),
)
@AssistedFactory
interface Factory : NFTComponent.Factory {
override fun create(context: AppComponentContext, params: NFTComponent.Params): DefaultNFTComponent

View file

@ -1,6 +1,7 @@
package com.tangem.features.nft.common
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.models.wallet.UserWalletId
@ -15,7 +16,7 @@ internal sealed class NFTRoute : Route {
@Serializable
data class Receive(
val userWalletId: UserWalletId,
val portfolioId: PortfolioId,
) : NFTRoute()
@Serializable

View file

@ -13,8 +13,8 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.nft.receive.model.NFTReceiveModel
import com.tangem.features.nft.receive.ui.NFTReceive
import com.tangem.features.tokenreceive.TokenReceiveComponent
@ -57,8 +57,7 @@ internal class NFTReceiveComponent @AssistedInject constructor(
)
data class Params(
val userWalletId: UserWalletId,
val walletName: String,
val portfolioId: PortfolioId,
val onBackClick: () -> Unit,
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.nft.receive.model
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.ui.account.toUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
@ -11,19 +12,28 @@ import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.fields.InputManager
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.FilterNFTAvailableNetworksUseCase
import com.tangem.domain.nft.GetNFTCurrencyUseCase
import com.tangem.domain.nft.GetNFTNetworkStatusUseCase
import com.tangem.domain.nft.GetNFTNetworksUseCase
import com.tangem.domain.nft.analytics.NFTAnalyticsEvent
import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.nft.impl.R
import com.tangem.features.nft.receive.NFTReceiveComponent
import com.tangem.features.nft.receive.entity.NFTReceiveUM
@ -53,6 +63,9 @@ internal class NFTReceiveModel @Inject constructor(
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
private val getNFTCurrencyUseCase: GetNFTCurrencyUseCase,
private val receiveAddressesFactory: ReceiveAddressesFactory,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val singleAccountSupplier: SingleAccountSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
paramsContainer: ParamsContainer,
) : Model() {
@ -63,10 +76,7 @@ internal class NFTReceiveModel @Inject constructor(
private val _state = MutableStateFlow(
value = NFTReceiveUM(
onBackClick = params.onBackClick,
appBarSubtitle = resourceReference(
R.string.hot_crypto_add_token_subtitle,
formatArgs = wrappedList(params.walletName),
),
appBarSubtitle = TextReference.EMPTY,
search = getInitialSearchBar(),
networks = NFTReceiveUM.Networks.Content(
availableItems = persistentListOf(),
@ -81,11 +91,44 @@ internal class NFTReceiveModel @Inject constructor(
init {
analyticsEventHandler.send(NFTAnalyticsEvent.Receive.ScreenOpened)
subscribeToNFTAvailableNetworks()
loadPortfolioName()
}
private fun loadPortfolioName() = modelScope.launch(dispatchers.default) {
val appBarSubtitle = when (val portfolioId = params.portfolioId) {
is PortfolioId.Wallet -> loadWalletName(portfolioId.userWalletId)
is PortfolioId.Account -> if (isAccountsModeEnabledUseCase.invokeSync()) {
loadAccountName(portfolioId.accountId)
} else {
loadWalletName(portfolioId.userWalletId)
}
}
_state.update { it.copy(appBarSubtitle = appBarSubtitle) }
}
private fun loadWalletName(userWalletId: UserWalletId): TextReference {
return getUserWalletUseCase(userWalletId)
.map { it.name }
.getOrNull()
?.let { createAppBarSubtitle(stringReference(it)) }
?: TextReference.EMPTY
}
private suspend fun loadAccountName(accountId: AccountId): TextReference {
return singleAccountSupplier
.getSyncOrNull(SingleAccountProducer.Params(accountId))
?.let { createAppBarSubtitle(it.accountName.toUM().value) }
?: TextReference.EMPTY
}
private fun createAppBarSubtitle(text: TextReference) = resourceReference(
R.string.hot_crypto_add_token_subtitle,
formatArgs = wrappedList(text),
)
private fun subscribeToNFTAvailableNetworks() {
combine(
flow = getNFTNetworksUseCase(params.userWalletId),
flow = getNFTNetworksUseCase(params.portfolioId),
flow2 = searchManager.query.distinctUntilChanged(),
) { networks, query ->
filterNFTAvailableNetworksUseCase(networks, query)
@ -98,6 +141,7 @@ internal class NFTReceiveModel @Inject constructor(
).transform(it)
}
}
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
@ -144,7 +188,7 @@ internal class NFTReceiveModel @Inject constructor(
analyticsEventHandler.send(NFTAnalyticsEvent.Receive.BlockchainChosen(network.name))
val networkStatus = getNFTNetworkStatusUseCase.invoke(
userWalletId = params.userWalletId,
userWalletId = params.portfolioId.userWalletId,
network = network,
) ?: return@launch
@ -191,7 +235,7 @@ internal class NFTReceiveModel @Inject constructor(
private suspend fun configureReceiveAddresses(addresses: NetworkAddress, network: Network): TokenReceiveConfig {
val cryptoCurrency = getNFTCurrencyUseCase.invoke(network)
return receiveAddressesFactory.createForNft(
userWalletId = params.userWalletId,
userWalletId = params.portfolioId.userWalletId,
addresses = addresses,
network = network,
nft = cryptoCurrency,