Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-14 12:24:12 +05:00
parent 514d6f5e17
commit d0b078a793
23 changed files with 495 additions and 141 deletions

View file

@ -66,4 +66,11 @@ internal object NFTDomainModule {
GetNFTNetworkStatusUseCase(
networksRepository = networksRepository,
)
@Provides
@Singleton
fun providesGetNFTExploreUrlUseCase(nftRepository: NFTRepository): GetNFTExploreUrlUseCase =
GetNFTExploreUrlUseCase(
nftRepository = nftRepository,
)
}

View file

@ -11,6 +11,7 @@ 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.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.nft.models.NFTCollections
import com.tangem.domain.nft.models.NFTSalePrice
@ -117,7 +118,7 @@ internal class DefaultNFTRepository @Inject constructor(
assets.forEach {
val assetId = NFTSdkAssetIdentifierConverter.convert(it.identifier)
val price = getNFTRuntimeStore(userWalletId, network).getSalePriceSync(assetId)
if (price is NFTSalePrice.Error) {
if (price is NFTSalePrice.Empty || price is NFTSalePrice.Error) {
refreshSalePrice(userWalletId, network, sdkCollectionId, it.identifier)
}
}
@ -155,6 +156,12 @@ internal class DefaultNFTRepository @Inject constructor(
override suspend fun isNFTSupported(network: Network): Boolean = network.canHandleNFTs()
override suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? =
walletManagersFacade.getNFTExploreUrl(
network = network,
assetIdentifier = NFTSdkAssetIdentifierConverter.convertBack(assetIdentifier),
)
private suspend fun refreshSalePrice(
userWalletId: UserWalletId,
network: Network,

View file

@ -709,6 +709,11 @@ class DefaultWalletManagersFacade(
return walletManager.getSalePrice(collectionIdentifier, assetIdentifier)
}
override suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? {
val blockchain = Blockchain.fromId(network.id.value)
return blockchain.getNFTExploreUrl(assetIdentifier)
}
override suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean {
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network)
val initializableAccountWalletManger = walletManager as? InitializableAccount ?: return true

View file

@ -273,6 +273,8 @@ interface WalletManagersFacade {
assetIdentifier: NFTAsset.Identifier,
): NFTAsset.SalePrice?
suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String?
/**
* If wallet manager implements [InitializableAccount] then returns [InitializableAccount.isAccountInitialized]
* value. Otherwise always return true

View file

@ -0,0 +1,21 @@
package com.tangem.domain.nft
import arrow.core.raise.catch
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.tokens.model.Network
class GetNFTExploreUrlUseCase(
private val nftRepository: NFTRepository,
) {
suspend operator fun invoke(network: Network, assetIdentifier: NFTAsset.Identifier): String? = catch(
block = {
nftRepository.getNFTExploreUrl(
network = network,
assetIdentifier = assetIdentifier,
)
},
catch = { null },
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.nft.repository
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.nft.models.NFTCollections
import com.tangem.domain.tokens.model.Network
@ -14,4 +15,6 @@ interface NFTRepository {
suspend fun refreshAssets(userWalletId: UserWalletId, network: Network, collectionId: NFTCollection.Identifier)
suspend fun isNFTSupported(network: Network): Boolean
suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String?
}

View file

@ -27,6 +27,7 @@ dependencies {
implementation(projects.core.datasource)
/** Domain modules */
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.models)
implementation(projects.domain.nft)
implementation(projects.domain.nft.models)

View file

@ -18,6 +18,7 @@ import com.tangem.features.nft.collections.NFTCollectionsComponent
import com.tangem.features.nft.common.ui.NFTContent
import com.tangem.features.nft.component.NFTComponent
import com.tangem.features.nft.details.NFTDetailsComponent
import com.tangem.features.nft.details.info.NFTDetailsInfoComponent
import com.tangem.features.nft.receive.NFTReceiveComponent
import com.tangem.features.nft.traits.NFTAssetTraitsComponent
import dagger.assisted.Assisted
@ -29,6 +30,7 @@ import kotlinx.coroutines.launch
internal class DefaultNFTComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: NFTComponent.Params,
private val nftDetailsInfoComponentFactory: NFTDetailsInfoComponent.Factory,
) : NFTComponent, AppComponentContext by appComponentContext {
private val stackNavigation = StackNavigation<NFTRoute>()
@ -142,6 +144,7 @@ internal class DefaultNFTComponent @AssistedInject constructor(
)
},
),
nftDetailsInfoComponentFactory = nftDetailsInfoComponentFactory,
)
private fun getAssetTraitsComponent(

View file

@ -4,11 +4,19 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.context.AppComponentContext
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.nft.models.NFTAsset
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.nft.details.entity.NFTDetailsBottomSheetConfig
import com.tangem.features.nft.details.info.NFTDetailsInfoComponent
import com.tangem.features.nft.details.model.NFTDetailsModel
import com.tangem.features.nft.details.ui.NFTDetails
import dagger.assisted.Assisted
@ -17,15 +25,40 @@ import dagger.assisted.AssistedInject
internal class NFTDetailsComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: Params,
private val nftDetailsInfoComponentFactory: NFTDetailsInfoComponent.Factory,
) : ComposableContentComponent, AppComponentContext by context {
private val model: NFTDetailsModel = getOrCreateModel(params)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = null,
handleBackButton = false,
childFactory = ::bottomSheetChild,
)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
NFTDetails(state, modifier)
bottomSheet.child?.instance?.BottomSheet()
}
private fun bottomSheetChild(
config: NFTDetailsBottomSheetConfig,
componentContext: ComponentContext,
): ComposableBottomSheetComponent = when (config) {
is NFTDetailsBottomSheetConfig.Info -> nftDetailsInfoComponentFactory.create(
context = childByContext(componentContext),
params = NFTDetailsInfoComponent.Params(
text = config.text,
onDismiss = {
model.bottomSheetNavigation.dismiss()
},
),
)
}
data class Params(

View file

@ -3,7 +3,9 @@ package com.tangem.features.nft.details.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
data class NFTAssetUM(
val name: String,
@ -37,7 +39,13 @@ data class NFTAssetUM(
sealed class SalePrice {
data object Loading : SalePrice()
data object Empty : SalePrice()
data class Content(val value: String, val fiatValue: String) : SalePrice()
data class Content(
val value: BigDecimal,
val symbol: String,
val decimals: Int,
val rate: BigDecimal?,
val appCurrency: AppCurrency,
) : SalePrice()
}
@Immutable

View file

@ -0,0 +1,12 @@
package com.tangem.features.nft.details.entity
import com.tangem.core.ui.extensions.TextReference
import kotlinx.serialization.Serializable
@Serializable
internal sealed interface NFTDetailsBottomSheetConfig {
@Serializable
data class Info(
val text: TextReference,
) : NFTDetailsBottomSheetConfig
}

View file

@ -1,7 +1,5 @@
package com.tangem.features.nft.details.entity
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
internal data class NFTDetailsUM(
val nftAsset: NFTAssetUM,
val onBackClick: () -> Unit,
@ -9,5 +7,4 @@ internal data class NFTDetailsUM(
val onSeeAllTraitsClick: () -> Unit,
val onExploreClick: () -> Unit,
val onSendClick: () -> Unit,
val bottomSheetConfig: TangemBottomSheetConfig?,
)

View file

@ -1,9 +0,0 @@
package com.tangem.features.nft.details.entity
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
internal data class NFTInfoBottomSheetConfig(
val title: TextReference,
val text: TextReference,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,124 @@
package com.tangem.features.nft.details.entity.factory
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTSalePrice
import com.tangem.features.nft.details.entity.NFTAssetUM
import com.tangem.features.nft.details.entity.NFTDetailsUM
import com.tangem.features.nft.impl.R
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
internal class NFTDetailsUMFactory(
private val onBackClick: () -> Unit,
private val onReadMoreClick: () -> Unit,
private val onSeeAllTraitsClick: () -> Unit,
private val onExploreClick: () -> Unit,
private val onSendClick: () -> Unit,
) {
fun getInitialState(nftAsset: NFTAsset): NFTDetailsUM = NFTDetailsUM(
nftAsset = nftAsset.transform(),
onBackClick = onBackClick,
onReadMoreClick = onReadMoreClick,
onSeeAllTraitsClick = onSeeAllTraitsClick,
onExploreClick = onExploreClick,
onSendClick = onSendClick,
)
private fun NFTAsset.transform(): NFTAssetUM = NFTAssetUM(
name = name.orEmpty(),
media = media?.let {
NFTAssetUM.Media.Content(
mimetype = it.mimetype,
url = it.url,
)
} ?: NFTAssetUM.Media.Empty,
topInfo = when {
salePrice is NFTSalePrice.Empty && description.isNullOrEmpty() && rarity == null -> {
NFTAssetUM.TopInfo.Empty
}
else -> {
val hasSalePrice = salePrice !is NFTSalePrice.Empty
val hasDescription = !description.isNullOrEmpty()
val rarity = rarity
NFTAssetUM.TopInfo.Content(
title = if (hasSalePrice) {
resourceReference(R.string.nft_details_last_sale_price)
} else {
null
},
salePrice = NFTAssetUM.SalePrice.Empty,
description = description,
rarity = if (rarity != null) {
NFTAssetUM.Rarity.Content(
rank = rarity.rank,
label = rarity.label,
showDivider = hasSalePrice || hasDescription,
)
} else {
NFTAssetUM.Rarity.Empty
},
)
}
},
traits = traits.take(MAX_TRAITS_COUNT).map {
NFTAssetUM.BlockItem(
title = stringReference(it.name),
value = it.value,
)
}.toImmutableList(),
baseInfoItems = buildBaseInfoItems(),
)
private fun NFTAsset.buildBaseInfoItems() = when (val id = id) {
is NFTAsset.Identifier.EVM -> persistentListOf(
NFTAssetUM.BlockItem(
title = resourceReference(R.string.nft_details_token_standard),
value = contractType,
),
NFTAssetUM.BlockItem(
title = resourceReference(R.string.nft_details_contract_address),
value = id.tokenAddress,
valueTextEllipsis = TextEllipsis.Middle,
),
NFTAssetUM.BlockItem(
title = resourceReference(R.string.nft_details_token_id),
value = id.tokenId,
),
NFTAssetUM.BlockItem(
title = resourceReference(R.string.nft_details_chain),
value = network.name,
),
)
is NFTAsset.Identifier.TON -> persistentListOf(
NFTAssetUM.BlockItem(
title = resourceReference(R.string.nft_details_token_address),
value = id.tokenAddress,
valueTextEllipsis = TextEllipsis.Middle,
),
NFTAssetUM.BlockItem(
title = resourceReference(R.string.nft_details_chain),
value = network.name,
),
)
is NFTAsset.Identifier.Solana -> persistentListOf(
NFTAssetUM.BlockItem(
title = resourceReference(R.string.nft_details_token_address),
value = id.tokenAddress,
valueTextEllipsis = TextEllipsis.Middle,
),
NFTAssetUM.BlockItem(
title = resourceReference(R.string.nft_details_chain),
value = network.name,
),
)
is NFTAsset.Identifier.Unknown -> persistentListOf()
}
companion object {
private const val MAX_TRAITS_COUNT = 6
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.features.nft.details.info
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.nft.details.info.ui.NFTInfoBottomSheet
import com.tangem.features.nft.details.info.ui.NFTInfoBottomSheetContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultNFTDetailsInfoComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: NFTDetailsInfoComponent.Params,
) : NFTDetailsInfoComponent, AppComponentContext by context {
override fun dismiss() {
params.onDismiss()
}
@Composable
override fun BottomSheet() {
val bottomSheetConfig = remember(key1 = this) {
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
)
}
NFTInfoBottomSheet(
config = bottomSheetConfig,
content = {
NFTInfoBottomSheetContent(
text = params.text,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16),
)
},
)
}
@AssistedFactory
interface Factory : NFTDetailsInfoComponent.Factory {
override fun create(
context: AppComponentContext,
params: NFTDetailsInfoComponent.Params,
): DefaultNFTDetailsInfoComponent
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.features.nft.details.info
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.extensions.TextReference
internal interface NFTDetailsInfoComponent : ComposableBottomSheetComponent {
data class Params(
val text: TextReference,
val onDismiss: () -> Unit,
)
interface Factory : ComponentFactory<Params, NFTDetailsInfoComponent>
}

View file

@ -0,0 +1,69 @@
package com.tangem.features.nft.details.info.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.impl.R
@Composable
internal fun NFTInfoBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = config,
titleText = resourceReference(R.string.nft_about_title),
content = { content() },
)
}
@Composable
internal fun NFTInfoBottomSheetContent(text: TextReference, modifier: Modifier = Modifier) {
val scrollState = rememberScrollState()
Column(
modifier = modifier.verticalScroll(scrollState),
) {
Text(
text = text.resolveReference(),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
)
}
}
@Composable
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_NFTInfoBottomSheetContent() {
TangemThemePreview {
NFTInfoBottomSheetContent(
text = stringReference(
"""
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce varius neque vel ligula
tincidunt, nec faucibus nulla ultricies. Maecenas euismod arcu in nunc volutpat,
at bibendum eros lacinia. Proin hendrerit massa non velit congue,
in volutpat nisi consequat. Sed vitae justo nec orci tincidunt malesuada.
Nullam feugiat purus vel lectus efficitur, vel fringilla urna volutpat.
Donec sagittis enim in metus lacinia, vel tempor nunc bibendum.
""".trimIndent(),
),
)
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.features.nft.details.model
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
@ -7,13 +9,18 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.nft.analytics.NFTAnalyticsEvent
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.nft.GetNFTExploreUrlUseCase
import com.tangem.features.nft.details.NFTDetailsComponent
import com.tangem.features.nft.details.entity.NFTAssetUM
import com.tangem.features.nft.details.entity.NFTDetailsBottomSheetConfig
import com.tangem.features.nft.details.entity.NFTDetailsUM
import com.tangem.features.nft.details.entity.factory.NFTDetailsUMFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@ModelScoped
@ -21,45 +28,57 @@ internal class NFTDetailsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val analyticsEventHandler: AnalyticsEventHandler,
private val urlOpener: UrlOpener,
private val getNFTExploreUrlUseCase: GetNFTExploreUrlUseCase,
paramsContainer: ParamsContainer,
) : Model() {
@Suppress("UnusedPrivateMember")
private val params: NFTDetailsComponent.Params = paramsContainer.require()
val state: StateFlow<NFTDetailsUM> get() = _state
private val _state = MutableStateFlow(
value = NFTDetailsUM(
nftAsset = NFTAssetUM(
name = "",
media = NFTAssetUM.Media.Empty,
topInfo = NFTAssetUM.TopInfo.Empty,
traits = persistentListOf(),
baseInfoItems = persistentListOf(),
),
onBackClick = params.onBackClick,
onReadMoreClick = ::onReadMoreClick,
onSeeAllTraitsClick = {
analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonSeeAll)
params.onAllTraitsClick()
},
onExploreClick = ::onExploreClick,
onSendClick = ::onSendClick,
bottomSheetConfig = null,
),
private val stateFactory: NFTDetailsUMFactory = NFTDetailsUMFactory(
onBackClick = { params.onBackClick() },
onReadMoreClick = ::onReadMoreClick,
onSeeAllTraitsClick = { params.onAllTraitsClick() },
onExploreClick = ::onExploreClick,
onSendClick = ::onSendClick,
)
private val _state = MutableStateFlow(
value = stateFactory.getInitialState(params.nftAsset),
)
val bottomSheetNavigation: SlotNavigation<NFTDetailsBottomSheetConfig> = SlotNavigation()
init {
analyticsEventHandler.send(NFTAnalyticsEvent.Details.ScreenOpened(params.nftAsset.network.name))
}
private fun onReadMoreClick() {
analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonReadMore)
when (val topInfo = _state.value.nftAsset.topInfo) {
is NFTAssetUM.TopInfo.Empty -> Unit
is NFTAssetUM.TopInfo.Content -> {
bottomSheetNavigation.activate(
NFTDetailsBottomSheetConfig.Info(
text = stringReference(topInfo.description.orEmpty()),
),
)
}
}
}
private fun onExploreClick() {
analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonExplore)
modelScope.launch {
val url = getNFTExploreUrlUseCase.invoke(
network = params.nftAsset.network,
assetIdentifier = params.nftAsset.id,
)
if (url != null) {
urlOpener.openUrl(url)
}
}
}
private fun onSendClick() {

View file

@ -11,12 +11,9 @@ import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.nft.details.entity.NFTDetailsUM
import com.tangem.features.nft.details.entity.NFTInfoBottomSheetConfig
import com.tangem.features.nft.details.ui.bottomsheet.NFTInfoBottomSheet
import com.tangem.features.nft.impl.R
@Composable
@ -45,7 +42,6 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) {
modifier = Modifier
.padding(innerPadding),
)
ShowBottomSheet(state.bottomSheetConfig)
},
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
@ -58,12 +54,4 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) {
)
},
)
}
@Composable
fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
if (bottomSheetConfig == null) return
when (bottomSheetConfig.content) {
is NFTInfoBottomSheetConfig -> NFTInfoBottomSheet(bottomSheetConfig)
}
}

View file

@ -16,9 +16,11 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.nft.details.entity.NFTAssetUM
import com.tangem.features.nft.impl.R
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
@Composable
internal fun NFTDetailsAsset(
@ -120,7 +122,13 @@ private class NFTAssetProvider : CollectionPreviewParameterProvider<NFTAssetUM>(
),
topInfo = NFTAssetUM.TopInfo.Content(
title = resourceReference(R.string.nft_details_last_sale_price),
salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"),
salePrice = NFTAssetUM.SalePrice.Content(
value = BigDecimal("18.75"),
symbol = "ETH",
decimals = 18,
rate = BigDecimal("1"),
appCurrency = AppCurrency.Default,
),
description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3),
rarity = NFTAssetUM.Rarity.Content(
rank = "Top 1% rarity",

View file

@ -3,24 +3,34 @@ package com.tangem.features.nft.details.ui
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.amountScreen.utils.getFiatString
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.ReadMoreText
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.nft.details.entity.NFTAssetUM
import com.tangem.features.nft.impl.R
import java.math.BigDecimal
private const val READ_MORE_MAX_LINES = 3
@Composable
internal fun NFTDetailsInfoGroup(
@ -41,7 +51,10 @@ internal fun NFTDetailsInfoGroup(
contentHorizontalPadding = 0.dp,
) {
Column(
modifier = Modifier,
modifier = Modifier
.padding(
bottom = TangemTheme.dimens.spacing12,
),
) {
SalePrice(state.salePrice)
Description(state.description, onReadMoreClick)
@ -68,7 +81,6 @@ private fun SalePrice(state: NFTAssetUM.SalePrice, modifier: Modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing12,
),
) {
TextShimmer(
@ -92,19 +104,23 @@ private fun SalePrice(state: NFTAssetUM.SalePrice, modifier: Modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing12,
),
verticalArrangement = Arrangement.SpaceAround,
) {
Text(
text = state.value,
text = state.value.format {
crypto(
symbol = state.symbol,
decimals = state.decimals,
)
},
style = TangemTheme.typography.head,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing4),
text = state.fiatValue,
text = getFiatString(state.value, state.rate, state.appCurrency),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
@ -117,18 +133,29 @@ private fun SalePrice(state: NFTAssetUM.SalePrice, modifier: Modifier = Modifier
@Composable
private fun Description(description: String?, onReadMoreClick: () -> Unit, modifier: Modifier = Modifier) {
if (!description.isNullOrEmpty()) {
// TODO configure right way to display 3 rows and "Read more" label
EllipsisText(
val interactionSource = remember { MutableInteractionSource() }
ReadMoreText(
modifier = modifier
.clickable { onReadMoreClick() }
.clickable(
interactionSource = interactionSource,
indication = null,
onClick = onReadMoreClick,
)
.padding(
horizontal = TangemTheme.dimens.spacing12,
vertical = TangemTheme.dimens.spacing8,
start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
),
expanded = false,
text = description,
style = TangemTheme.typography.body2.copy(
color = TangemTheme.colors.text.tertiary,
),
readMoreText = resourceReference(R.string.common_read_more).resolveReference(),
readMoreMaxLines = READ_MORE_MAX_LINES,
readMoreStyle = TangemTheme.typography.body2.copy(
color = TangemTheme.colors.text.accent,
).toSpanStyle(),
)
}
}
@ -141,7 +168,9 @@ private fun Rarity(state: NFTAssetUM.Rarity, modifier: Modifier = Modifier) {
if (state.showDivider) {
HorizontalDivider(
modifier = Modifier.padding(
horizontal = TangemTheme.dimens.spacing12,
start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing8,
end = TangemTheme.dimens.spacing12,
),
color = TangemTheme.colors.stroke.primary,
)
@ -150,8 +179,9 @@ private fun Rarity(state: NFTAssetUM.Rarity, modifier: Modifier = Modifier) {
modifier = modifier
.fillMaxWidth()
.padding(
horizontal = TangemTheme.dimens.spacing6,
vertical = TangemTheme.dimens.spacing8,
start = TangemTheme.dimens.spacing6,
top = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing6,
),
) {
NFTDetailsGroupBlock(
@ -159,7 +189,6 @@ private fun Rarity(state: NFTAssetUM.Rarity, modifier: Modifier = Modifier) {
.weight(1f)
.padding(
horizontal = TangemTheme.dimens.spacing6,
vertical = TangemTheme.dimens.spacing4,
),
title = resourceReference(R.string.nft_details_rarity_label),
value = stringReference(state.label),
@ -169,7 +198,6 @@ private fun Rarity(state: NFTAssetUM.Rarity, modifier: Modifier = Modifier) {
.weight(1f)
.padding(
horizontal = TangemTheme.dimens.spacing6,
vertical = TangemTheme.dimens.spacing4,
),
title = resourceReference(R.string.nft_details_rarity_rank),
value = stringReference(state.rank),
@ -196,7 +224,13 @@ private class NFTAssetInfoProvider : CollectionPreviewParameterProvider<NFTAsset
collection = listOf(
NFTAssetUM.TopInfo.Content(
title = resourceReference(R.string.nft_details_last_sale_price),
salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"),
salePrice = NFTAssetUM.SalePrice.Content(
value = BigDecimal("18.75"),
symbol = "ETH",
decimals = 18,
rate = BigDecimal("1"),
appCurrency = AppCurrency.Default,
),
description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3),
rarity = NFTAssetUM.Rarity.Content(
rank = "Top 1% rarity",
@ -216,7 +250,13 @@ private class NFTAssetInfoProvider : CollectionPreviewParameterProvider<NFTAsset
),
NFTAssetUM.TopInfo.Content(
title = resourceReference(R.string.nft_details_last_sale_price),
salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"),
salePrice = NFTAssetUM.SalePrice.Content(
value = BigDecimal("18.75"),
symbol = "ETH",
decimals = 18,
rate = BigDecimal("1"),
appCurrency = AppCurrency.Default,
),
description = null,
rarity = NFTAssetUM.Rarity.Content(
rank = "Top 1% rarity",
@ -226,13 +266,25 @@ private class NFTAssetInfoProvider : CollectionPreviewParameterProvider<NFTAsset
),
NFTAssetUM.TopInfo.Content(
title = resourceReference(R.string.nft_details_last_sale_price),
salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"),
salePrice = NFTAssetUM.SalePrice.Content(
value = BigDecimal("18.75"),
symbol = "ETH",
decimals = 18,
rate = BigDecimal("1"),
appCurrency = AppCurrency.Default,
),
description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3),
rarity = NFTAssetUM.Rarity.Empty,
),
NFTAssetUM.TopInfo.Content(
title = resourceReference(R.string.nft_details_last_sale_price),
salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"),
salePrice = NFTAssetUM.SalePrice.Content(
value = BigDecimal("18.75"),
symbol = "ETH",
decimals = 18,
rate = BigDecimal("1"),
appCurrency = AppCurrency.Default,
),
description = null,
rarity = NFTAssetUM.Rarity.Empty,
),

View file

@ -1,73 +0,0 @@
package com.tangem.features.nft.details.ui.bottomsheet
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.details.entity.NFTInfoBottomSheetConfig
@Composable
fun NFTInfoBottomSheet(config: TangemBottomSheetConfig) {
val scrollState = rememberScrollState()
TangemBottomSheet<NFTInfoBottomSheetConfig>(
config = config,
title = { content ->
TangemBottomSheetTitle(title = content.title)
},
) { content ->
Column(
modifier = Modifier.verticalScroll(scrollState),
) {
Text(
text = content.text.resolveReference(),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
)
}
}
}
@Composable
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_StakingInfoBottomSheet() {
TangemThemePreview {
NFTInfoBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = NFTInfoBottomSheetConfig(
title = stringReference("Title"),
text = stringReference(
"""
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce varius neque vel ligula
tincidunt, nec faucibus nulla ultricies. Maecenas euismod arcu in nunc volutpat,
at bibendum eros lacinia. Proin hendrerit massa non velit congue,
in volutpat nisi consequat. Sed vitae justo nec orci tincidunt malesuada.
Nullam feugiat purus vel lectus efficitur, vel fringilla urna volutpat.
Donec sagittis enim in metus lacinia, vel tempor nunc bibendum.
""".trimIndent(),
),
),
),
)
}
}

View file

@ -8,6 +8,8 @@ import com.tangem.features.nft.collections.model.NFTCollectionsModel
import com.tangem.features.nft.common.DefaultNFTComponent
import com.tangem.features.nft.component.*
import com.tangem.features.nft.details.block.DefaultNFTDetailsBlockComponent
import com.tangem.features.nft.details.info.DefaultNFTDetailsInfoComponent
import com.tangem.features.nft.details.info.NFTDetailsInfoComponent
import com.tangem.features.nft.details.model.NFTDetailsModel
import com.tangem.features.nft.receive.model.NFTReceiveModel
import com.tangem.features.nft.traits.model.NFTAssetTraitsModel
@ -34,6 +36,12 @@ internal object NFTFeatureModule {
@Module
@InstallIn(SingletonComponent::class)
internal interface NFTFeatureModuleBinds {
@Binds
@Singleton
fun bindNFTDetailsInfoComponentFactory(
factory: DefaultNFTDetailsInfoComponent.Factory,
): NFTDetailsInfoComponent.Factory
@Binds
@Singleton
fun bindNFTComponentFactory(impl: DefaultNFTComponent.Factory): NFTComponent.Factory