diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index a8cb6b030d..de24b448ec 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -66,4 +66,11 @@ internal object NFTDomainModule { GetNFTNetworkStatusUseCase( networksRepository = networksRepository, ) + + @Provides + @Singleton + fun providesGetNFTExploreUrlUseCase(nftRepository: NFTRepository): GetNFTExploreUrlUseCase = + GetNFTExploreUrlUseCase( + nftRepository = nftRepository, + ) } \ No newline at end of file diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 197249ec85..7b806c688a 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -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, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 1e16b212a1..ba14ca68b2 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -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 diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index b40c497186..45a93fff76 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -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 diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTExploreUrlUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTExploreUrlUseCase.kt new file mode 100644 index 0000000000..d6794fd7fe --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTExploreUrlUseCase.kt @@ -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 }, + ) +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt index 5d811e9a39..36ca075957 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt @@ -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? } \ No newline at end of file diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index 7aba5590b7..df09cb6d6a 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -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) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt index 1c85ec5d39..95e4a2ad8f 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt @@ -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() @@ -142,6 +144,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( ) }, ), + nftDetailsInfoComponentFactory = nftDetailsInfoComponentFactory, ) private fun getAssetTraitsComponent( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/NFTDetailsComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/NFTDetailsComponent.kt index 274d87d25e..2333a6a0e0 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/NFTDetailsComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/NFTDetailsComponent.kt @@ -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( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt index 2b85b58fe8..22243c075c 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt @@ -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 diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsBottomSheetConfig.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsBottomSheetConfig.kt new file mode 100644 index 0000000000..e8478f38ac --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsBottomSheetConfig.kt @@ -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 +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt index 67955452f9..95844969ae 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt @@ -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?, ) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTInfoBottomSheetConfig.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTInfoBottomSheetConfig.kt deleted file mode 100644 index 3cb7f5273c..0000000000 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTInfoBottomSheetConfig.kt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt new file mode 100644 index 0000000000..faf5d73b1d --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt @@ -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 + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/DefaultNFTDetailsInfoComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/DefaultNFTDetailsInfoComponent.kt new file mode 100644 index 0000000000..26dc1a1dd6 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/DefaultNFTDetailsInfoComponent.kt @@ -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 + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/NFTDetailsInfoComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/NFTDetailsInfoComponent.kt new file mode 100644 index 0000000000..90650da63b --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/NFTDetailsInfoComponent.kt @@ -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 +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/ui/NFTInfoBottomSheet.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/ui/NFTInfoBottomSheet.kt new file mode 100644 index 0000000000..b95179836f --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/ui/NFTInfoBottomSheet.kt @@ -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( + 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(), + ), + ) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index 918cfd4ab6..e81bb43891 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -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 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 = 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() { diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt index 1fba75b717..b662151b8d 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt @@ -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) - } } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt index b077835ec8..233c56d281 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt @@ -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( ), 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", diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt index aa34278977..8f71b0c87c 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt @@ -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( - 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(), - ), - ), - ), - ) - } -} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt index abc85dcd0a..9c3ac6fbb8 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt @@ -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