Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-08 12:32:41 +05:00
parent dfaf4e4e77
commit ce4df5bee6
18 changed files with 417 additions and 8 deletions

View file

@ -15,6 +15,7 @@ import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.nft.component.NFTCollectionsComponent
import com.tangem.features.nft.component.NFTDetailsComponent
import com.tangem.features.nft.component.NFTReceiveComponent
import com.tangem.features.nft.component.NFTAssetTraitsComponent
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onramp.component.*
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
@ -87,6 +88,7 @@ internal class ChildFactory @Inject constructor(
private val nftCollectionsComponentFactory: NFTCollectionsComponent.Factory,
private val nftReceiveComponentFactory: NFTReceiveComponent.Factory,
private val nftDetailsComponentFactory: NFTDetailsComponent.Factory,
private val nftAssetTraitsComponentFactory: NFTAssetTraitsComponent.Factory,
private val nftSendComponentFactory: NFTSendComponent.Factory,
private val testerRouter: TesterRouter,
private val routingFeatureToggles: RoutingFeatureToggles,
@ -429,6 +431,12 @@ internal class ChildFactory @Inject constructor(
),
componentFactory = nftDetailsComponentFactory,
)
is AppRoute.NFTAssetTraits ->
createComponentChild(
context = context,
params = NFTAssetTraitsComponent.Params(nftAsset = route.nftAsset),
componentFactory = nftAssetTraitsComponentFactory,
)
is AppRoute.NFTSend -> {
createComponentChild(
context = context,
@ -796,6 +804,12 @@ internal class ChildFactory @Inject constructor(
),
componentFactory = nftDetailsComponentFactory,
)
is AppRoute.NFTAssetTraits ->
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = NFTAssetTraitsComponent.Params(nftAsset = route.nftAsset),
componentFactory = nftAssetTraitsComponentFactory,
)
is AppRoute.NFTSend -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),

View file

@ -297,6 +297,11 @@ sealed class AppRoute(val path: String) : Route {
val collectionName: String,
) : AppRoute(path = "/nft_details/${userWalletId.stringValue}/${nftAsset.collectionId}/${nftAsset.id.stringValue}")
@Serializable
data class NFTAssetTraits(
val nftAsset: NFTAsset,
) : AppRoute(path = "/nft_traits/${nftAsset.collectionId}/${nftAsset.id.stringValue}")
@Serializable
data class NFTSend(
val userWalletId: UserWalletId,

View file

@ -532,6 +532,7 @@
<string name="nft_wallet_receive_nft">Tap here to receive first NFT</string>
<string name="nft_wallet_title">NFT collections</string>
<string name="nft_wallet_unable_to_load">Unable to load the data</string>
<string name="nft_traits_title">Traits</string>
<string name="onboarding_access_code_feature_1_description">Set up a single access code to protect all your devices.</string>
<string name="onboarding_access_code_feature_1_title">Protect</string>
<string name="onboarding_access_code_feature_2_description">Set an individual access code for each card or ring later.</string>

View file

@ -0,0 +1,14 @@
package com.tangem.features.nft.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.nft.models.NFTAsset
interface NFTAssetTraitsComponent : ComposableContentComponent {
data class Params(
val nftAsset: NFTAsset,
)
interface Factory : ComponentFactory<Params, NFTAssetTraitsComponent>
}

View file

@ -6,7 +6,7 @@ internal data class NFTDetailsUM(
val nftAsset: NFTAssetUM,
val onBackClick: () -> Unit,
val onReadMoreClick: () -> Unit,
val onSeeAllClick: () -> Unit,
val onSeeAllTraitsClick: () -> Unit,
val onExploreClick: () -> Unit,
val onSendClick: () -> Unit,
val bottomSheetConfig: TangemBottomSheetConfig?,

View file

@ -0,0 +1,9 @@
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

@ -1,5 +1,6 @@
package com.tangem.features.nft.details.model
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -36,7 +37,7 @@ internal class NFTDetailsModel @Inject constructor(
),
onBackClick = ::navigateBack,
onReadMoreClick = ::onReadMoreClick,
onSeeAllClick = ::onSeeAllClick,
onSeeAllTraitsClick = ::onSeeAllTraitsClick,
onExploreClick = ::onExploreClick,
onSendClick = ::onSendClick,
bottomSheetConfig = null,
@ -47,8 +48,12 @@ internal class NFTDetailsModel @Inject constructor(
// TODO implement
}
private fun onSeeAllClick() {
// TODO implement
private fun onSeeAllTraitsClick() {
router.push(
AppRoute.NFTAssetTraits(
nftAsset = params.nftAsset,
),
)
}
private fun onExploreClick() {

View file

@ -11,9 +11,12 @@ 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
@ -37,11 +40,12 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) {
NFTDetailsAsset(
state = state.nftAsset,
onReadMoreClick = state.onReadMoreClick,
onSeeAllClick = state.onSeeAllClick,
onSeeAllTraitsClick = state.onSeeAllTraitsClick,
onExploreClick = state.onExploreClick,
modifier = Modifier
.padding(innerPadding),
)
ShowBottomSheet(state.bottomSheetConfig)
},
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
@ -54,4 +58,12 @@ 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

@ -24,7 +24,7 @@ import kotlinx.collections.immutable.persistentListOf
internal fun NFTDetailsAsset(
state: NFTAssetUM,
onReadMoreClick: () -> Unit,
onSeeAllClick: () -> Unit,
onSeeAllTraitsClick: () -> Unit,
onExploreClick: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -72,7 +72,7 @@ internal fun NFTDetailsAsset(
NFTBlocksGroupAction(
text = resourceReference(R.string.common_see_all),
startIcon = { },
onClick = onSeeAllClick,
onClick = onSeeAllTraitsClick,
)
},
)
@ -103,7 +103,7 @@ private fun Preview_NFTDetailsAssetAsset(@PreviewParameter(NFTAssetProvider::cla
NFTDetailsAsset(
state = state,
onReadMoreClick = { },
onSeeAllClick = { },
onSeeAllTraitsClick = { },
onExploreClick = { },
)
}

View file

@ -0,0 +1,73 @@
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

@ -10,11 +10,14 @@ import com.tangem.features.nft.component.NFTCollectionsComponent
import com.tangem.features.nft.component.NFTDetailsBlockComponent
import com.tangem.features.nft.component.NFTDetailsComponent
import com.tangem.features.nft.component.NFTReceiveComponent
import com.tangem.features.nft.component.NFTAssetTraitsComponent
import com.tangem.features.nft.details.DefaultNFTDetailsComponent
import com.tangem.features.nft.details.block.DefaultNFTDetailsBlockComponent
import com.tangem.features.nft.details.model.NFTDetailsModel
import com.tangem.features.nft.receive.DefaultNFTReceiveComponent
import com.tangem.features.nft.receive.model.NFTReceiveModel
import com.tangem.features.nft.traits.DefaultNFTAssetTraitsComponent
import com.tangem.features.nft.traits.model.NFTAssetTraitsModel
import dagger.Binds
import dagger.Module
import dagger.Provides
@ -72,4 +75,13 @@ internal interface NFTFeatureModuleBinds {
fun bindNFTDetailsBlockComponentFactory(
impl: DefaultNFTDetailsBlockComponent.Factory,
): NFTDetailsBlockComponent.Factory
@Binds
@Singleton
fun bindNFTTraitsComponentFactory(impl: DefaultNFTAssetTraitsComponent.Factory): NFTAssetTraitsComponent.Factory
@Binds
@IntoMap
@ClassKey(NFTAssetTraitsModel::class)
fun bindNFTTraitsModel(model: NFTAssetTraitsModel): Model
}

View file

@ -0,0 +1,37 @@
package com.tangem.features.nft.traits
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.nft.component.NFTAssetTraitsComponent
import com.tangem.features.nft.traits.ui.NFTAssetTraits
import com.tangem.features.nft.traits.model.NFTAssetTraitsModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultNFTAssetTraitsComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: NFTAssetTraitsComponent.Params,
) : NFTAssetTraitsComponent, AppComponentContext by context {
private val model: NFTAssetTraitsModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
NFTAssetTraits(state)
}
@AssistedFactory
interface Factory : NFTAssetTraitsComponent.Factory {
override fun create(
context: AppComponentContext,
params: NFTAssetTraitsComponent.Params,
): DefaultNFTAssetTraitsComponent
}
}

View file

@ -0,0 +1,7 @@
package com.tangem.features.nft.traits.entity
data class NFTAssetTraitUM(
val id: String,
val name: String,
val value: String,
)

View file

@ -0,0 +1,8 @@
package com.tangem.features.nft.traits.entity
import kotlinx.collections.immutable.ImmutableList
data class NFTAssetTraitsUM(
val onBackClick: () -> Unit,
val traits: ImmutableList<NFTAssetTraitUM>,
)

View file

@ -0,0 +1,48 @@
package com.tangem.features.nft.traits.model
import com.tangem.core.decompose.di.ModelScoped
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.models.NFTAsset
import com.tangem.features.nft.component.NFTAssetTraitsComponent
import com.tangem.features.nft.traits.entity.NFTAssetTraitUM
import com.tangem.features.nft.traits.entity.NFTAssetTraitsUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ModelScoped
internal class NFTAssetTraitsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
paramsContainer: ParamsContainer,
) : Model() {
private val params: NFTAssetTraitsComponent.Params = paramsContainer.require()
val state: StateFlow<NFTAssetTraitsUM> get() = _state
private val _state = MutableStateFlow(
value = NFTAssetTraitsUM(
traits = params.nftAsset.transform(),
onBackClick = ::navigateBack,
),
)
private fun NFTAsset.transform(): ImmutableList<NFTAssetTraitUM> = this
.traits
.mapIndexed { index, trait ->
NFTAssetTraitUM(
id = index.toString(),
name = trait.name,
value = trait.value,
)
}.toPersistentList()
private fun navigateBack() {
router.pop()
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.features.nft.traits.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.nft.traits.entity.NFTAssetTraitUM
@Composable
internal fun NFTAssetTrait(state: NFTAssetTraitUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
) {
Text(
text = state.name,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
Text(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing4),
text = state.value,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.features.nft.traits.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.nft.impl.R
import com.tangem.features.nft.traits.entity.NFTAssetTraitsUM
@Composable
internal fun NFTAssetTraits(state: NFTAssetTraitsUM, modifier: Modifier = Modifier) {
BackHandler(onBack = state.onBackClick)
Scaffold(
modifier = modifier,
containerColor = TangemTheme.colors.background.secondary,
topBar = {
TangemTopAppBar(
modifier = Modifier.statusBarsPadding(),
startButton = TopAppBarButtonUM(
iconRes = R.drawable.ic_back_24,
onIconClicked = state.onBackClick,
),
title = stringResourceSafe(R.string.nft_traits_title),
)
},
content = { innerPadding ->
NFTAssetTraitsContent(
modifier = Modifier
.padding(innerPadding),
state = state,
)
},
)
}

View file

@ -0,0 +1,94 @@
package com.tangem.features.nft.traits.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.runtime.Composable
import androidx.compose.runtime.key
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.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.traits.entity.NFTAssetTraitUM
import com.tangem.features.nft.traits.entity.NFTAssetTraitsUM
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun NFTAssetTraitsContent(state: NFTAssetTraitsUM, modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier
.padding(TangemTheme.dimens.spacing16),
title = null,
contentHorizontalPadding = 0.dp,
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState()),
) {
state.traits.forEach { trait ->
key(trait.id) {
NFTAssetTrait(
modifier = Modifier
.padding(
horizontal = TangemTheme.dimens.spacing12,
vertical = TangemTheme.dimens.spacing8,
),
state = trait,
)
}
}
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_NFTDetailsAssetAsset(@PreviewParameter(NFTAssetTraitsProvider::class) state: NFTAssetTraitsUM) {
TangemThemePreview {
NFTAssetTraitsContent(
state = state,
)
}
}
private class NFTAssetTraitsProvider : CollectionPreviewParameterProvider<NFTAssetTraitsUM>(
collection = listOf(
NFTAssetTraitsUM(
traits = persistentListOf(
NFTAssetTraitUM(
id = "1",
name = "Trait 1",
value = "Value",
),
NFTAssetTraitUM(
id = "2",
name = "Trait 2",
value = "Value",
),
NFTAssetTraitUM(
id = "3",
name = "Trait 3",
value = "Value",
),
NFTAssetTraitUM(
id = "4",
name = "Trait 4",
value = "Value",
),
NFTAssetTraitUM(
id = "5",
name = "Trait 5",
value = "Value",
),
),
onBackClick = { },
),
),
)