Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-29 16:16:48 +04:00
parent df938519a2
commit 6cd6210a05
9 changed files with 292 additions and 129 deletions

View file

@ -35,7 +35,7 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
private val portfolioComponent = portfolioComponentFactory.create(
context = child("my_portfolio"),
params = MarketsPortfolioComponent.Params(params.token.id),
params = MarketsPortfolioComponent.Params(params.token),
)
init {

View file

@ -4,13 +4,14 @@ import androidx.compose.runtime.Stable
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import kotlinx.serialization.Serializable
@Stable
interface MarketsPortfolioComponent : ComposableContentComponent {
@Serializable
data class Params(val tokenId: String)
data class Params(val token: TokenMarketParams)
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>)

View file

@ -5,13 +5,12 @@ import arrow.core.getOrElse
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
@ -23,9 +22,8 @@ import javax.inject.Inject
@ComponentScoped
internal class MarketsPortfolioModel @Inject constructor(
paramsContainer: ParamsContainer,
getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val portfolioDataLoader: PortfolioDataLoader,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
@ -34,59 +32,134 @@ internal class MarketsPortfolioModel @Inject constructor(
private val params = paramsContainer.require<MarketsPortfolioComponent.Params>()
private val availableNetworks = MutableStateFlow<List<TokenMarketInfo.Network>?>(value = null)
private val availableNetworksFlow = MutableStateFlow<List<TokenMarketInfo.Network>?>(value = null)
private val factory = MyPortfolioUMMFactory(::onAddClick, ::onTokenItemClick)
/** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */
private val selectedMultiWalletIdFlow = MutableStateFlow<UserWalletId?>(value = null)
/** Map of [UserWalletId] and network ids that user is changed */
private val walletsWithChangedNetworksFlow = MutableStateFlow<Map<UserWalletId, List<String>>>(value = emptyMap())
private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel())
private val factory = MyPortfolioUMMFactory(
onAddClick = { onAddToPortfolioBSVisibilityChange(isShow = true) },
onTokenItemClick = ::onTokenItemClick,
addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory(
token = params.token,
onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange,
onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange,
onNetworkSwitchClick = ::onNetworkSwitchClick,
onWalletSelect = ::onWalletSelect,
onContinueClick = ::onContinueClick,
),
)
init {
combine(
flow = getAllWalletsCryptoCurrencyStatusesUseCase(params.tokenId),
flow2 = availableNetworks,
flow3 = getSelectedAppCurrencyUseCase(),
flow4 = getBalanceHidingSettingsUseCase(),
) { walletsWithCurrencyStatuses, networks, maybeAppCurrency, isBalanceHidden ->
val appCurrency = maybeAppCurrency.getOrElse { e ->
Timber.e("Failed to load app currency: $e")
AppCurrency.Default
}
// Subscribe on selected wallet flow to support actual selected wallet
subscribeOnSelectedMultiWalletUpdates()
factory.create(
walletsWithCurrencyStatuses = walletsWithCurrencyStatuses,
availableNetworks = networks,
appCurrency = appCurrency,
isBalanceHidden = isBalanceHidden.isBalanceHidden,
)
}
subscribeOnStateUpdates()
}
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
availableNetworksFlow.value = networks
}
fun setNoNetworksAvailable() {
availableNetworksFlow.value = emptyList()
}
private fun subscribeOnSelectedMultiWalletUpdates() {
getSelectedWalletUseCase()
.getOrElse { e ->
Timber.e("Failed to load selected wallet: $e")
error("Failed to load selected wallet")
}
.onEach {
selectedMultiWalletIdFlow.value = it.takeIf { it.isMultiCurrency }?.walletId
}
.launchIn(modelScope)
}
private fun subscribeOnStateUpdates() {
combine(
flow = portfolioDataLoader.load(params.token.id),
flow2 = portfolioBSVisibilityModelFlow,
flow3 = availableNetworksFlow,
flow4 = selectedMultiWalletIdFlow,
flow5 = walletsWithChangedNetworksFlow,
transform = factory::create,
)
.onEach { _state.value = it }
.launchIn(modelScope)
}
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
availableNetworks.value = networks
}
fun setNoNetworksAvailable() {
availableNetworks.value = emptyList()
}
private fun onAddClick() {
// TODO [REDACTED_JIRA]
}
private fun onTokenItemClick(token: CryptoCurrencyStatus) {
private fun onTokenItemClick(index: Int, id: CryptoCurrency.ID) {
_state.update {
(_state.value as? MyPortfolioUM.Tokens)?.let { state ->
state.copy(
tokens = state.tokens.map { tokenUM ->
if (tokenUM.tokenItemState.id == token.currency.id.value) {
val state = _state.value as? MyPortfolioUM.Tokens ?: return@update it
state.copy(
tokens = state.tokens
.mapIndexed { i, tokenUM ->
// check by index and id because id can be the same
if (index == i && id.value == tokenUM.tokenItemState.id) {
tokenUM.copy(isQuickActionsShown = !tokenUM.isQuickActionsShown)
} else {
tokenUM
}
}
.toImmutableList(),
)
} ?: it
.toImmutableList(),
)
}
}
private fun onNetworkSwitchClick(id: String, isChecked: Boolean) {
walletsWithChangedNetworksFlow.update {
val selectedWalletId = selectedMultiWalletIdFlow.value
if (selectedWalletId == null) {
Timber.e("Impossible ti switch network when selected wallet is null")
return@update it
}
it.toMutableMap().apply {
val networkIds = this[selectedWalletId] ?: emptyList()
if (isChecked) {
this[selectedWalletId] = networkIds + id
} else {
this[selectedWalletId] = networkIds - id
}
}
}
}
private fun onWalletSelect(userWalletId: UserWalletId) {
selectedMultiWalletIdFlow.update { prevUserWalletId ->
// Clear user changes if user select another wallet
walletsWithChangedNetworksFlow.update {
it.toMutableMap().apply { remove(prevUserWalletId) }
}
userWalletId
}
}
private fun onContinueClick() {
// TODO [REDACTED_JIRA]
}
private fun onAddToPortfolioBSVisibilityChange(isShow: Boolean) {
portfolioBSVisibilityModelFlow.update {
it.copy(addToPortfolioBSVisibility = isShow, walletSelectorBSVisibility = false)
}
}
private fun onWalletSelectorVisibilityChange(isShow: Boolean) {
portfolioBSVisibilityModelFlow.update {
it.copy(addToPortfolioBSVisibility = true, walletSelectorBSVisibility = isShow)
}
}
}

View file

@ -1,11 +1,12 @@
package com.tangem.features.markets.portfolio.impl.model
import arrow.core.Either
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
/**
@ -18,63 +19,142 @@ import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
*/
internal class MyPortfolioUMMFactory(
private val onAddClick: () -> Unit,
private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit,
private val onTokenItemClick: (Int, CryptoCurrency.ID) -> Unit,
private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory,
) {
/**
* Create [MyPortfolioUM] from data set
*
* @param walletsWithCurrencyStatuses wallets with currency statuses
* @param availableNetworks available networks
* @param appCurrency app currency
* @param isBalanceHidden flag that indicates if balance should be hidden
*/
fun create(
walletsWithCurrencyStatuses: Map<UserWallet, List<Either<CurrencyStatusError, CryptoCurrencyStatus>>>,
portfolioModel: PortfolioData,
portfolioBSVisibilityModel: PortfolioBSVisibilityModel,
availableNetworks: List<TokenMarketInfo.Network>?,
appCurrency: AppCurrency,
isBalanceHidden: Boolean,
selectedWalletId: UserWalletId?,
walletsWithChangedNetworks: Map<UserWalletId, List<String>>,
): MyPortfolioUM {
if (availableNetworks == null) return MyPortfolioUM.Loading
if (availableNetworks.isEmpty()) return MyPortfolioUM.Unavailable
val availableWalletsWithStatuses = walletsWithCurrencyStatuses
.filterAvailable(networks = availableNetworks)
.flatMapStatuses()
val walletsWithStatuses = portfolioModel.walletsWithCurrencyStatuses
.filterAvailableNetworks(networks = availableNetworks)
if (availableWalletsWithStatuses.isEmpty()) {
return MyPortfolioUM.AddFirstToken(onAddClick = onAddClick)
val isPortfolioEmpty = walletsWithStatuses.flatMap { it.value }.isEmpty()
if (isPortfolioEmpty) {
val hasMultiWallets = walletsWithStatuses.filterKeys(UserWallet::isMultiCurrency).isNotEmpty()
return if (hasMultiWallets) {
MyPortfolioUM.AddFirstToken(
bsConfig = createAddToPortfolioBSConfig(
portfolioModel = portfolioModel,
selectedWalletId = selectedWalletId,
availableNetworks = availableNetworks,
walletsWithChangedNetworks = walletsWithChangedNetworks,
portfolioBSVisibilityModel = portfolioBSVisibilityModel,
),
onAddClick = onAddClick,
)
} else {
MyPortfolioUM.Unavailable
}
}
return TokensPortfolioUMConverter(
appCurrency = appCurrency,
isBalanceHidden = isBalanceHidden,
availableNetworks = availableNetworks,
appCurrency = portfolioModel.appCurrency,
isBalanceHidden = portfolioModel.isBalanceHidden,
isAllAvailableNetworksAdded = walletsWithStatuses.isAllAvailableNetworksAdded(availableNetworks),
bsConfig = createAddToPortfolioBSConfig(
portfolioModel = portfolioModel,
selectedWalletId = selectedWalletId,
availableNetworks = availableNetworks,
portfolioBSVisibilityModel = portfolioBSVisibilityModel,
walletsWithChangedNetworks = walletsWithChangedNetworks,
),
onAddClick = onAddClick,
onTokenItemClick = onTokenItemClick,
)
.convert(availableWalletsWithStatuses)
.convert(walletsWithStatuses)
}
private fun Map<UserWallet, List<Either<CurrencyStatusError, CryptoCurrencyStatus>>>.filterAvailable(
networks: List<TokenMarketInfo.Network>,
): Map<UserWallet, List<CryptoCurrencyStatus>> {
val networkIds = networks.map { it.networkId }
private fun createAddToPortfolioBSConfig(
portfolioModel: PortfolioData,
selectedWalletId: UserWalletId?,
availableNetworks: List<TokenMarketInfo.Network>,
portfolioBSVisibilityModel: PortfolioBSVisibilityModel,
walletsWithChangedNetworks: Map<UserWalletId, List<String>>,
): TangemBottomSheetConfig {
val walletId = requireNotNull(selectedWalletId) {
"Selected wallet must be not null in state with AddToPortfolio bottom sheet"
}
return mapValues { entry ->
entry.value.mapNotNull { maybeStatus ->
maybeStatus.getOrNull()
?.takeIf { networkIds.contains(it.currency.network.backendId) }
}
val selectedWallet = portfolioModel.walletsWithCurrencyStatuses.keys
.firstOrNull { it.walletId == walletId }
?: error("portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId")
val changedNetworks = walletsWithChangedNetworks[selectedWalletId]
val alreadyAddedNetworks = requireNotNull(
value = portfolioModel.walletsWithCurrencyStatuses[selectedWallet],
lazyMessage = {
"portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId"
},
)
.filterAvailableNetworks(availableNetworks)
.map { it.currency.network.backendId }
return addToPortfolioBSContentUMFactory.create(
portfolioData = portfolioModel,
portfolioBSVisibilityModel = portfolioBSVisibilityModel,
selectedWallet = selectedWallet,
networksWithToggle = availableNetworks.associateWithToggle(
changedNetworks = changedNetworks,
alreadyAddedNetworks = alreadyAddedNetworks,
),
isUserChangedNetworks = changedNetworks != null && alreadyAddedNetworks != changedNetworks,
)
}
private fun List<TokenMarketInfo.Network>.associateWithToggle(
changedNetworks: List<String>?,
alreadyAddedNetworks: List<String>,
): Map<TokenMarketInfo.Network, Boolean> {
// Use user choice or check already added networks
return associateWith { network ->
val isSelectedByUser = changedNetworks?.contains(network.networkId)
if (isSelectedByUser != null) return@associateWith isSelectedByUser
val isAlreadyAdded = alreadyAddedNetworks.any { it == network.networkId }
isAlreadyAdded
}
}
private fun Map<UserWallet, List<CryptoCurrencyStatus>>.flatMapStatuses(): Map<UserWallet, CryptoCurrencyStatus> {
private fun Map<UserWallet, List<CryptoCurrencyStatus>>.isAllAvailableNetworksAdded(
availableNetworks: List<TokenMarketInfo.Network>,
): Boolean {
val networkIds = availableNetworks.map { it.networkId }
return this
.flatMap { entry ->
entry.value.map { entry.key to it }
}
.toMap()
// User can add currencies only in multi-currency wallets
.filterKeys(UserWallet::isMultiCurrency)
.mapValues { it.value.map { it.currency.network.backendId } }
// Each wallets contains all available networks?
.all { it.value.containsAll(networkIds) }
}
/** Filter map values by available networks [networks] */
private fun Map<UserWallet, List<CryptoCurrencyStatus>>.filterAvailableNetworks(
networks: List<TokenMarketInfo.Network>,
): Map<UserWallet, List<CryptoCurrencyStatus>> {
return mapValues { entry -> entry.value.filterAvailableNetworks(networks) }
}
/** Filter list of [CryptoCurrencyStatus] by available networks [networks] */
private fun List<CryptoCurrencyStatus>.filterAvailableNetworks(
networks: List<TokenMarketInfo.Network>,
): List<CryptoCurrencyStatus> {
val networkIds = networks.map(TokenMarketInfo.Network::networkId)
return mapNotNull {
it.takeIf { networkIds.contains(it.currency.network.backendId) }
}
}
}

View file

@ -17,9 +17,9 @@ internal class PortfolioTokenUMConverter(
private val appCurrency: AppCurrency,
private val isBalanceHidden: Boolean,
private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit,
) : Converter<Map.Entry<UserWallet, CryptoCurrencyStatus>, PortfolioTokenUM> {
) : Converter<Pair<UserWallet, CryptoCurrencyStatus>, PortfolioTokenUM> {
override fun convert(value: Map.Entry<UserWallet, CryptoCurrencyStatus>): PortfolioTokenUM {
override fun convert(value: Pair<UserWallet, CryptoCurrencyStatus>): PortfolioTokenUM {
val (userWallet, cryptoCurrencyStatus) = value
val tokenItemStateConverter = TokenItemStateConverter(

View file

@ -1,7 +1,8 @@
package com.tangem.features.markets.portfolio.impl.model
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
@ -17,42 +18,29 @@ import kotlinx.collections.immutable.toImmutableList
internal class TokensPortfolioUMConverter(
private val appCurrency: AppCurrency,
private val isBalanceHidden: Boolean,
private val availableNetworks: List<TokenMarketInfo.Network>,
private val isAllAvailableNetworksAdded: Boolean,
private val bsConfig: TangemBottomSheetConfig,
private val onAddClick: () -> Unit,
private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit,
) : Converter<Map<UserWallet, CryptoCurrencyStatus>, MyPortfolioUM.Tokens> {
private val onTokenItemClick: (index: Int, id: CryptoCurrency.ID) -> Unit,
) : Converter<Map<UserWallet, List<CryptoCurrencyStatus>>, MyPortfolioUM.Tokens> {
override fun convert(value: Map<UserWallet, CryptoCurrencyStatus>): MyPortfolioUM.Tokens {
override fun convert(value: Map<UserWallet, List<CryptoCurrencyStatus>>): MyPortfolioUM.Tokens {
return MyPortfolioUM.Tokens(
tokens = PortfolioTokenUMConverter(appCurrency, isBalanceHidden, onTokenItemClick)
.convertList(value.entries)
tokens = value
.flatMap { entry ->
entry.value.map { entry.key to it }
}
.mapIndexed { index, status ->
PortfolioTokenUMConverter(
appCurrency = appCurrency,
isBalanceHidden = isBalanceHidden,
onTokenItemClick = { onTokenItemClick(index, it.currency.id) },
).convert(status)
}
.toImmutableList(),
buttonState = getAddButtonState(
availableWalletsWithStatuses = value,
availableNetworks = availableNetworks,
),
buttonState = if (isAllAvailableNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available,
bsConfig = bsConfig,
onAddClick = onAddClick,
)
}
private fun getAddButtonState(
availableWalletsWithStatuses: Map<UserWallet, CryptoCurrencyStatus>,
availableNetworks: List<TokenMarketInfo.Network>,
): AddButtonState {
val networkIds = availableNetworks.map { it.networkId }
val allNetworksAdded = availableWalletsWithStatuses
// User can add currencies only in multi-currency wallets
.filterKeys(UserWallet::isMultiCurrency)
.entries
.groupBy(
keySelector = { it.key },
valueTransform = { it.value.currency.network.backendId },
)
.mapValues { it.value.toSet() }
// Each wallets contains all available networks?
.all { it.value.containsAll(networkIds) }
return if (allNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available
}
}

View file

@ -53,7 +53,11 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) {
}
}
AddToPortfolioBottomSheet(config = state.addToPortfolioBS)
when (state) {
is MyPortfolioUM.AddFirstToken -> AddToPortfolioBottomSheet(config = state.bsConfig)
is MyPortfolioUM.Tokens -> AddToPortfolioBottomSheet(config = state.bsConfig)
else -> Unit
}
}
@Composable

View file

@ -1,6 +1,8 @@
package com.tangem.features.markets.portfolio.impl.ui.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
@ -14,19 +16,39 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfol
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken, sampleToken),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Available,
bsConfig = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
onAddClick = {},
),
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable,
bsConfig = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
onAddClick = {},
),
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading,
bsConfig = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
onAddClick = {},
),
MyPortfolioUM.AddFirstToken(
bsConfig = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
onAddClick = {},
),
MyPortfolioUM.Loading,

View file

@ -2,21 +2,15 @@ package com.tangem.features.markets.portfolio.impl.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed class MyPortfolioUM {
val addToPortfolioBS: TangemBottomSheetConfig = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
)
data class Tokens(
val tokens: ImmutableList<PortfolioTokenUM>,
val buttonState: AddButtonState,
val bsConfig: TangemBottomSheetConfig,
val onAddClick: () -> Unit,
) : MyPortfolioUM() {
@ -28,6 +22,7 @@ internal sealed class MyPortfolioUM {
}
data class AddFirstToken(
val bsConfig: TangemBottomSheetConfig,
val onAddClick: () -> Unit,
) : MyPortfolioUM()