Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-10 16:59:23 +07:00
parent 98b8c6b58e
commit 17d7b65e51
7 changed files with 108 additions and 63 deletions

View file

@ -34,6 +34,7 @@ class IsAccountsModeEnabledUseCase(
combine(flows = totalAccountsCountList) { it.toList().isModeEnabled() } combine(flows = totalAccountsCountList) { it.toList().isModeEnabled() }
} }
.onEmpty { emit(false) } .onEmpty { emit(false) }
.distinctUntilChanged()
} }
suspend fun invokeSync(): Boolean { suspend fun invokeSync(): Boolean {

View file

@ -25,6 +25,7 @@ dependencies {
/* Domain */ /* Domain */
implementation(projects.domain.account) implementation(projects.domain.account)
implementation(projects.domain.account.status)
implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models) implementation(projects.domain.appCurrency.models)
implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding)

View file

@ -4,40 +4,51 @@ import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
data class AvailableToAddData( internal data class AvailableToAddData(
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet>, val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet>,
) { ) {
val availableToAdd: Boolean
get() = availableToAddWallets.isNotEmpty()
val isSinglePortfolio: Boolean val isSinglePortfolio: Boolean
get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1 get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1
} }
data class AvailableToAddWallet( internal data class AvailableToAddWallet(
val userWallet: UserWallet, val userWallet: UserWallet,
val accounts: Set<AccountStatus>, val accounts: Set<AccountStatus>,
val availableNetworks: Set<TokenMarketInfo.Network>, val availableNetworks: Set<TokenMarketInfo.Network>,
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount>, val availableToAddAccounts: Map<AccountId, AvailableToAddAccount>,
) {
val isSingleAvailableNetworks: Boolean get() = availableNetworks.size == 1
val singleAvailableNetworks: TokenMarketInfo.Network get() = availableNetworks.first()
}
data class AvailableToAddAccount(
val account: AccountStatus,
val availableNetworks: Set<TokenMarketInfo.Network>,
val availableToAddNetworks: Set<TokenMarketInfo.Network>,
) )
data class SelectedPortfolio( internal data class AvailableToAddAccount(
val userWallet: UserWallet,
val account: AccountStatus, val account: AccountStatus,
val availableNetworks: Set<TokenMarketInfo.Network>,
val addedNetworks: Set<Network>,
) {
val isSingleNetwork: Boolean
get() = availableNetworks.size == 1
val availableToAddNetworks: Set<TokenMarketInfo.Network> = availableNetworks
.filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } }
.toSet()
val addedMarketNetworks: Set<TokenMarketInfo.Network> = availableNetworks
.filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } }
.toSet()
}
internal data class SelectedPortfolio(
val userWallet: UserWallet,
val account: AvailableToAddAccount,
val isAccountMode: Boolean, val isAccountMode: Boolean,
val availableMorePortfolio: Boolean, val availableMorePortfolio: Boolean,
) )
data class SelectedNetwork( internal data class SelectedNetwork(
val selectedNetwork: TokenMarketInfo.Network, val selectedNetwork: TokenMarketInfo.Network,
val cryptoCurrency: CryptoCurrency, val cryptoCurrency: CryptoCurrency,
val availableMoreNetwork: Boolean, val availableMoreNetwork: Boolean,

View file

@ -1,6 +1,7 @@
package com.tangem.features.markets.portfolio.add.impl package com.tangem.features.markets.portfolio.add.impl
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
@ -8,6 +9,7 @@ import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.markets.portfolio.add.api.SelectedNetwork import com.tangem.features.markets.portfolio.add.api.SelectedNetwork
import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio
import com.tangem.features.markets.portfolio.add.impl.model.AddTokenModel import com.tangem.features.markets.portfolio.add.impl.model.AddTokenModel
@ -46,7 +48,7 @@ internal class AddTokenComponent @AssistedInject constructor(
interface Callbacks { interface Callbacks {
fun onChangeNetworkClick() fun onChangeNetworkClick()
fun onChangePortfolioClick() fun onChangePortfolioClick()
fun onTokenAdded() fun onTokenAdded(status: CryptoCurrencyStatus)
} }
@AssistedFactory @AssistedFactory

View file

@ -1,9 +1,13 @@
package com.tangem.features.markets.portfolio.add.impl.converter package com.tangem.features.markets.portfolio.add.impl.converter
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.account.PortfolioFetcher import com.tangem.features.account.PortfolioFetcher
@ -12,33 +16,42 @@ import com.tangem.features.markets.portfolio.add.api.AvailableToAddData
import com.tangem.features.markets.portfolio.add.api.AvailableToAddWallet import com.tangem.features.markets.portfolio.add.api.AvailableToAddWallet
import javax.inject.Inject import javax.inject.Inject
class AvailableToAddDataConverter @Inject constructor( internal class AvailableToAddDataConverter @Inject constructor(
private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase, private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase,
private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
) { ) {
suspend fun convert( suspend fun convert(
balances: Map<UserWallet, PortfolioFetcher.PortfolioBalance>, balances: Map<UserWallet, PortfolioFetcher.PortfolioBalance>,
availableNetworks: Set<TokenMarketInfo.Network>, availableNetworks: Set<TokenMarketInfo.Network>,
marketParams: TokenMarketParams,
): AvailableToAddData { ): AvailableToAddData {
fun AccountStatus.getAvailableToAddAccount(): AvailableToAddAccount { suspend fun AccountStatus.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount {
val availableToAddNetworks = availableNetworks val addedNetworks = availableNetworks
.filterTo(mutableSetOf()) { network -> !this.isAlreadyAddedNetworks(network) } .mapNotNull { createCryptoCurrency(wallet, it, marketParams) }
.mapNotNull { getAccountCurrencyStatusUseCase.invokeSync(wallet.walletId, it) }
.mapNotNull { it.getOrNull()?.status?.currency?.network }
.toSet()
return AvailableToAddAccount( return AvailableToAddAccount(
account = this, account = this,
availableNetworks = availableNetworks, availableNetworks = availableNetworks,
availableToAddNetworks = availableToAddNetworks, addedNetworks = addedNetworks,
) )
} }
fun UserWallet.getAvailableToAddWallet(balance: PortfolioFetcher.PortfolioBalance): AvailableToAddWallet { suspend fun getAvailableToAddWallet(
val filteredNetworks = this.filteredAvailableNetworks(availableNetworks) entry: Map.Entry<UserWallet, PortfolioFetcher.PortfolioBalance>,
): AvailableToAddWallet {
val (wallet, balance) = entry
val filteredNetworks = wallet.filteredAvailableNetworks(availableNetworks)
val accounts = balance.accountsBalance.accountStatuses val accounts = balance.accountsBalance.accountStatuses
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount> = accounts val availableToAddAccounts: Map<AccountId, AvailableToAddAccount> = accounts
.map { it.account.accountId to it.getAvailableToAddAccount() } .map { it.account.accountId to it.getAvailableToAddAccount(wallet) }
.filter { (_, account) -> account.availableToAddNetworks.isNotEmpty() } .filter { (_, account) -> account.availableToAddNetworks.isNotEmpty() }
.toMap() .toMap()
return AvailableToAddWallet( return AvailableToAddWallet(
userWallet = this, userWallet = wallet,
accounts = accounts, accounts = accounts,
availableNetworks = filteredNetworks, availableNetworks = filteredNetworks,
availableToAddAccounts = availableToAddAccounts, availableToAddAccounts = availableToAddAccounts,
@ -46,8 +59,9 @@ class AvailableToAddDataConverter @Inject constructor(
} }
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet> = balances val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet> = balances
.map { (wallet, balance) -> .map {
val availableToAddWallet = wallet.getAvailableToAddWallet(balance) val (wallet, balance) = it
val availableToAddWallet = getAvailableToAddWallet(it)
wallet.walletId to availableToAddWallet wallet.walletId to availableToAddWallet
} }
.filter { (_, wallet) -> wallet.availableToAddAccounts.isNotEmpty() } .filter { (_, wallet) -> wallet.availableToAddAccounts.isNotEmpty() }
@ -64,8 +78,13 @@ class AvailableToAddDataConverter @Inject constructor(
networks = networks, networks = networks,
) )
private fun AccountStatus.isAlreadyAddedNetworks(network: TokenMarketInfo.Network): Boolean = when (this) { private suspend fun createCryptoCurrency(
is AccountStatus.CryptoPortfolio -> this.tokenList.flattenCurrencies() userWallet: UserWallet,
.any { status -> network.networkId == status.currency.network.backendId } network: TokenMarketInfo.Network,
} marketParams: TokenMarketParams,
): CryptoCurrency? = getTokenMarketCryptoCurrency(
userWalletId = userWallet.walletId,
tokenMarketParams = marketParams,
network = network,
)
} }

View file

@ -4,7 +4,8 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.markets.SaveMarketTokensUseCase import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.SaveCryptoCurrenciesUseCase
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
import com.tangem.features.markets.portfolio.add.api.SelectedNetwork import com.tangem.features.markets.portfolio.add.api.SelectedNetwork
import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio
@ -20,13 +21,15 @@ import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ModelScoped @ModelScoped
@Suppress("LongParameterList")
internal class AddTokenModel @Inject constructor( internal class AddTokenModel @Inject constructor(
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
private val saveMarketTokensUseCase: SaveMarketTokensUseCase,
private val uiBuilder: AddTokenUiBuilder, private val uiBuilder: AddTokenUiBuilder,
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase, private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
private val saveCryptoCurrenciesUseCase: SaveCryptoCurrenciesUseCase,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
) : Model() { ) : Model() {
private val params = paramsContainer.require<AddTokenComponent.Params>() private val params = paramsContainer.require<AddTokenComponent.Params>()
@ -37,13 +40,10 @@ internal class AddTokenModel @Inject constructor(
field = MutableStateFlow(value = null) field = MutableStateFlow(value = null)
init { init {
channelFlow { combine(
val selectedNetwork = params.selectedNetwork.stateIn(this) flow = params.selectedNetwork.distinctUntilChanged(),
val selectedPortfolio = params.selectedPortfolio.stateIn(this) flow2 = params.selectedPortfolio.distinctUntilChanged(),
combine( transform = { selectedNetwork, selectedPortfolio ->
flow = selectedNetwork,
flow2 = selectedPortfolio,
) { selectedNetwork, selectedPortfolio ->
addTokenJob.cancel() addTokenJob.cancel()
val isTangemIconVisible = needColdWalletInteraction(selectedNetwork, selectedPortfolio) val isTangemIconVisible = needColdWalletInteraction(selectedNetwork, selectedPortfolio)
uiBuilder.updateContent( uiBuilder.updateContent(
@ -52,29 +52,35 @@ internal class AddTokenModel @Inject constructor(
isTangemIconVisible = isTangemIconVisible, isTangemIconVisible = isTangemIconVisible,
onConfirmClick = { onAddClick(selectedNetwork, selectedPortfolio).saveIn(addTokenJob) }, onConfirmClick = { onAddClick(selectedNetwork, selectedPortfolio).saveIn(addTokenJob) },
) )
}.collect { newUI -> send(newUI) } },
} )
.flowOn(dispatchers.default)
.onEach { newUI -> uiState.value = newUI } .onEach { newUI -> uiState.value = newUI }
.flowOn(dispatchers.default)
.launchIn(modelScope) .launchIn(modelScope)
} }
private fun onAddClick(selectedNetwork: SelectedNetwork, selectedPortfolio: SelectedPortfolio) = modelScope.launch { private fun onAddClick(selectedNetwork: SelectedNetwork, selectedPortfolio: SelectedPortfolio) =
val um = uiState.value ?: return@launch modelScope.launch(dispatchers.default) {
uiState.value = um.toggleProgress(true) val um = uiState.value ?: return@launch
val blockchainNames = listOf(selectedNetwork.selectedNetwork) uiState.value = um.toggleProgress(true)
.mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name } val blockchainNames = listOf(selectedNetwork.selectedNetwork)
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames)) .mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name }
// todo account analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames))
saveMarketTokensUseCase( val cryptoCurrency = selectedNetwork.cryptoCurrency
userWalletId = selectedPortfolio.userWallet.walletId, val accountId = selectedPortfolio.account.account.account.accountId
tokenMarketParams = params.marketParams, saveCryptoCurrenciesUseCase(
addedNetworks = setOf(selectedNetwork.selectedNetwork), accountId = accountId,
removedNetworks = setOf(), add = listOf(cryptoCurrency),
) remove = listOf(),
params.callbacks.onTokenAdded() )
uiState.value = um.toggleProgress(false) val status = getAccountCurrencyStatusUseCase.invokeSync(
} userWalletId = accountId.userWalletId,
currencyId = cryptoCurrency.id,
network = cryptoCurrency.network,
).getOrNull() ?: return@launch
params.callbacks.onTokenAdded(status.status)
uiState.value = um.toggleProgress(false)
}
private suspend fun needColdWalletInteraction( private suspend fun needColdWalletInteraction(
selectedNetwork: SelectedNetwork, selectedNetwork: SelectedNetwork,

View file

@ -16,9 +16,10 @@ import com.tangem.features.markets.portfolio.add.api.SelectedNetwork
import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio
import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent
import com.tangem.features.markets.portfolio.add.impl.ui.state.AddTokenUM import com.tangem.features.markets.portfolio.add.impl.ui.state.AddTokenUM
import javax.inject.Inject
@ModelScoped @ModelScoped
internal class AddTokenUiBuilder( internal class AddTokenUiBuilder @Inject constructor(
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
) { ) {
private val params = paramsContainer.require<AddTokenComponent.Params>() private val params = paramsContainer.require<AddTokenComponent.Params>()
@ -41,9 +42,10 @@ internal class AddTokenUiBuilder(
portfolioName = stringReference(selectedPortfolio.userWallet.name) portfolioName = stringReference(selectedPortfolio.userWallet.name)
} }
true -> { true -> {
portfolioName = selectedPortfolio.account.account.accountName.toUM().value val accountStatus = selectedPortfolio.account.account
accountIcon = when (selectedPortfolio.account) { portfolioName = accountStatus.account.accountName.toUM().value
is AccountStatus.CryptoPortfolio -> selectedPortfolio.account.account.icon.toUM() accountIcon = when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.account.icon.toUM()
} }
} }
} }
@ -61,8 +63,11 @@ internal class AddTokenUiBuilder(
isTangemIconVisible: Boolean, isTangemIconVisible: Boolean,
onConfirmClick: () -> Unit, onConfirmClick: () -> Unit,
): AddTokenUM { ): AddTokenUM {
// its may happens when change portfolio after selected both params in line navigation
val isAvailableNetwork = selectedPortfolio.account.availableToAddNetworks
.any { selectedNetwork.selectedNetwork.networkId == it.networkId }
val button = AddTokenUM.Button( val button = AddTokenUM.Button(
isEnabled = true, isEnabled = isAvailableNetwork,
showProgress = false, showProgress = false,
isTangemIconVisible = isTangemIconVisible, isTangemIconVisible = isTangemIconVisible,
text = resourceReference(R.string.common_add), text = resourceReference(R.string.common_add),