Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-01 16:21:50 +05:00
parent 6ddf9a25ae
commit e8f2ed8517
22 changed files with 463 additions and 81 deletions

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.wallets.models.UserWalletId
interface NFTReceiveComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
)
interface Factory : ComponentFactory<Params, NFTReceiveComponent>
}

View file

@ -5,6 +5,9 @@ import com.tangem.core.decompose.model.Model
import com.tangem.features.nft.collections.DefaultNFTCollectionsComponent
import com.tangem.features.nft.collections.model.NFTCollectionsModel
import com.tangem.features.nft.component.NFTCollectionsComponent
import com.tangem.features.nft.component.NFTReceiveComponent
import com.tangem.features.nft.receive.DefaultNFTReceiveComponent
import com.tangem.features.nft.receive.model.NFTReceiveModel
import dagger.Binds
import dagger.Module
import dagger.Provides
@ -30,10 +33,21 @@ internal object NFTFeatureModule {
internal interface NFTFeatureModuleBinds {
@Binds
@Singleton
fun bindComponentFactory(impl: DefaultNFTCollectionsComponent.Factory): NFTCollectionsComponent.Factory
fun bindNFTCollectionsComponentFactory(
impl: DefaultNFTCollectionsComponent.Factory,
): NFTCollectionsComponent.Factory
@Binds
@IntoMap
@ClassKey(NFTCollectionsModel::class)
fun bindModel(model: NFTCollectionsModel): Model
fun bindNFTCollectionsModel(model: NFTCollectionsModel): Model
@Binds
@Singleton
fun bindNFTReceiveComponentFactory(impl: DefaultNFTReceiveComponent.Factory): NFTReceiveComponent.Factory
@Binds
@IntoMap
@ClassKey(NFTReceiveModel::class)
fun bindNFTReceiveModel(model: NFTReceiveModel): Model
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.nft.collections.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
@ -11,7 +12,8 @@ import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase
import com.tangem.domain.nft.GetNFTCollectionsUseCase
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.features.nft.collections.entity.*
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
import com.tangem.features.nft.collections.entity.transformer.ChangeCollectionExpandedStateTransformer
import com.tangem.features.nft.collections.entity.transformer.ToggleSearchBarTransformer
import com.tangem.features.nft.collections.entity.transformer.UpdateDataStateTransformer
@ -119,7 +121,11 @@ internal class NFTCollectionsModel @Inject constructor(
}
private fun onReceiveClick() {
// TODO move to receive
router.push(
AppRoute.NFTReceive(
userWalletId = params.userWalletId,
),
)
}
private fun navigateBack() {

View file

@ -0,0 +1,37 @@
package com.tangem.features.nft.receive
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.NFTReceiveComponent
import com.tangem.features.nft.receive.model.NFTReceiveModel
import com.tangem.features.nft.receive.ui.NFTReceive
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultNFTReceiveComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: NFTReceiveComponent.Params,
) : NFTReceiveComponent, AppComponentContext by context {
private val model: NFTReceiveModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
NFTReceive(state, modifier)
}
@AssistedFactory
interface Factory : NFTReceiveComponent.Factory {
override fun create(
context: AppComponentContext,
params: NFTReceiveComponent.Params,
): DefaultNFTReceiveComponent
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.features.nft.receive.entity
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import kotlinx.collections.immutable.ImmutableList
internal data class NFTReceiveNetworksUM(
val searchBar: SearchBarUM,
val networks: ImmutableList<NFTNetworkUM>,
)

View file

@ -0,0 +1,23 @@
package com.tangem.features.nft.receive.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import kotlinx.collections.immutable.ImmutableList
internal data class NFTReceiveUM(
val onBackClick: () -> Unit,
val search: SearchBarUM,
val networks: Networks,
val bottomSheetConfig: TangemBottomSheetConfig?,
) {
@Immutable
sealed class Networks {
data object Empty : Networks()
data class Content(
val items: ImmutableList<NFTNetworkUM>,
) : Networks()
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.nft.receive.entity.transformer
import com.tangem.features.nft.receive.entity.NFTReceiveUM
import com.tangem.utils.transformer.Transformer
internal class ToggleSearchBarTransformer(private val isActive: Boolean) : Transformer<NFTReceiveUM> {
override fun transform(prevState: NFTReceiveUM): NFTReceiveUM = prevState.copy(
search = prevState.search.copy(
isActive = isActive,
),
)
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.nft.receive.entity.transformer
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.domain.tokens.model.Network
import com.tangem.features.nft.receive.entity.NFTNetworkUM
import com.tangem.features.nft.receive.entity.NFTReceiveUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
internal class UpdateDataStateTransformer(
private val networks: List<Network>,
private val onNetworkClick: (Network) -> Unit,
) : Transformer<NFTReceiveUM> {
override fun transform(prevState: NFTReceiveUM): NFTReceiveUM = prevState.copy(
networks = when {
networks.isEmpty() -> NFTReceiveUM.Networks.Empty
else -> NFTReceiveUM.Networks.Content(
items = networks
.map { it.transform() }
.toPersistentList(),
)
},
)
private fun Network.transform(): NFTNetworkUM = NFTNetworkUM(
id = id.value,
iconRes = getActiveIconRes(id.value),
name = name,
onItemClick = {
onNetworkClick(this)
},
)
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.nft.receive.entity.transformer
import com.tangem.features.nft.receive.entity.NFTReceiveUM
import com.tangem.utils.transformer.Transformer
internal class UpdateSearchQueryTransformer(private val newQuery: String) : Transformer<NFTReceiveUM> {
override fun transform(prevState: NFTReceiveUM): NFTReceiveUM = prevState.copy(
search = prevState.search.copy(
query = newQuery,
),
)
}

View file

@ -0,0 +1,100 @@
package com.tangem.features.nft.receive.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.core.ui.components.fields.InputManager
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.nft.FilterNFTAvailableNetworksUseCase
import com.tangem.domain.nft.GetNFTAvailableNetworksUseCase
import com.tangem.domain.tokens.model.Network
import com.tangem.features.nft.component.NFTReceiveComponent
import com.tangem.features.nft.impl.R
import com.tangem.features.nft.receive.entity.NFTReceiveUM
import com.tangem.features.nft.receive.entity.transformer.ToggleSearchBarTransformer
import com.tangem.features.nft.receive.entity.transformer.UpdateDataStateTransformer
import com.tangem.features.nft.receive.entity.transformer.UpdateSearchQueryTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@ModelScoped
internal class NFTReceiveModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val searchManager: InputManager,
private val getNFTAvailableNetworksUseCase: GetNFTAvailableNetworksUseCase,
private val filterNFTAvailableNetworksUseCase: FilterNFTAvailableNetworksUseCase,
paramsContainer: ParamsContainer,
) : Model() {
val state: StateFlow<NFTReceiveUM> get() = _state
private val _state = MutableStateFlow(
value = NFTReceiveUM(
onBackClick = ::navigateBack,
search = getInitialSearchBar(),
networks = NFTReceiveUM.Networks.Content(persistentListOf()),
bottomSheetConfig = null,
),
)
private val params: NFTReceiveComponent.Params = paramsContainer.require()
init {
subscribeToNFTAvailableNetworks()
}
private fun subscribeToNFTAvailableNetworks() {
combine(
flow = getNFTAvailableNetworksUseCase(params.userWalletId),
flow2 = searchManager.query.distinctUntilChanged(),
) { networks, query ->
filterNFTAvailableNetworksUseCase(networks, query)
}
.onEach { filteredNetworks ->
_state.update {
UpdateDataStateTransformer(
networks = filteredNetworks,
onNetworkClick = ::onNetworkClick,
).transform(it)
}
}
.launchIn(modelScope)
}
private fun onSearchQueryChange(newQuery: String) {
modelScope.launch {
_state.update { UpdateSearchQueryTransformer(newQuery).transform(it) }
searchManager.update(newQuery)
}
}
private fun getInitialSearchBar(): SearchBarUM = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
isActive = false,
onQueryChange = ::onSearchQueryChange,
onActiveChange = ::toggleSearchBar,
)
private fun toggleSearchBar(isActive: Boolean) {
_state.update {
ToggleSearchBarTransformer(isActive).transform(it)
}
}
@Suppress("UnusedPrivateMember")
private fun onNetworkClick(network: Network) {
// TODO show receive bottom sheet
}
private fun navigateBack() {
router.pop()
}
}

View file

@ -0,0 +1,66 @@
package com.tangem.features.nft.receive.ui
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.*
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.components.fields.SearchBar
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
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.receive.entity.NFTReceiveUM
@Composable
internal fun NFTReceive(state: NFTReceiveUM, 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_close_24,
onIconClicked = state.onBackClick,
),
title = stringResourceSafe(id = R.string.nft_receive_title),
subtitle = stringResourceSafe(id = R.string.nft_receive_subtitle),
)
},
content = { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
) {
SearchBar(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
state = state.search,
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
)
val contentModifier = Modifier
.padding(TangemTheme.dimens.spacing16)
AnimatedContent(
targetState = state.networks,
contentKey = { it::class },
label = "NFT Receive",
) {
when (val networks = it) {
is NFTReceiveUM.Networks.Content -> NFTReceiveNetworksContent(networks, contentModifier)
is NFTReceiveUM.Networks.Empty -> NFTReceiveNetworksEmpty(contentModifier)
}
}
}
},
)
}

View file

@ -1,9 +1,8 @@
package com.tangem.features.nft.receive.ui
import android.content.res.Configuration
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@ -14,57 +13,39 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.impl.R
import com.tangem.features.nft.receive.entity.NFTNetworkUM
import com.tangem.features.nft.receive.entity.NFTReceiveNetworksUM
import com.tangem.features.nft.receive.entity.NFTReceiveUM
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun NFTReceiveNetworks(state: NFTReceiveNetworksUM, modifier: Modifier = Modifier) {
internal fun NFTReceiveNetworksContent(state: NFTReceiveUM.Networks.Content, modifier: Modifier = Modifier) {
val listState = rememberLazyListState()
Column(
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors.background.secondary)
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.primary)
.animateContentSize(),
state = listState,
) {
SearchBar(
state = state.searchBar,
// TODO fix colors after merging [REDACTED_TASK_KEY]
)
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(
vertical = TangemTheme.dimens.spacing16,
)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.primary),
state = listState,
item(
key = "title",
) {
item(
key = "title",
) {
Title()
}
items(
items = state.networks,
key = NFTNetworkUM::id,
) { network ->
NFTNetwork(state = network)
}
Title()
}
items(
items = state.items,
key = NFTNetworkUM::id,
) { network ->
NFTNetwork(
state = network,
modifier = Modifier.animateItem(),
)
}
}
}
@ -91,16 +72,9 @@ private fun Title(modifier: Modifier = Modifier) {
@Composable
private fun Preview_NFTReceiveNetworksUM() {
TangemThemePreview {
NFTReceiveNetworks(
state = NFTReceiveNetworksUM(
searchBar = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
onQueryChange = { },
isActive = false,
onActiveChange = { },
),
networks = persistentListOf(
NFTReceiveNetworksContent(
state = NFTReceiveUM.Networks.Content(
items = persistentListOf(
NFTNetworkUM(
id = "item1",
name = "Ethereum",

View file

@ -0,0 +1,18 @@
package com.tangem.features.nft.receive.ui
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.nft.impl.R
@Composable
internal fun NFTReceiveNetworksEmpty(modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = stringResourceSafe(id = R.string.nft_empty_search),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}