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

@ -25,6 +25,7 @@ dependencies {
/* Domain */
implementation(projects.domain.account)
implementation(projects.domain.account.status)
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
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.AccountStatus
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.UserWalletId
data class AvailableToAddData(
internal data class AvailableToAddData(
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet>,
) {
val availableToAdd: Boolean
get() = availableToAddWallets.isNotEmpty()
val isSinglePortfolio: Boolean
get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1
}
data class AvailableToAddWallet(
internal data class AvailableToAddWallet(
val userWallet: UserWallet,
val accounts: Set<AccountStatus>,
val availableNetworks: Set<TokenMarketInfo.Network>,
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(
val userWallet: UserWallet,
internal data class AvailableToAddAccount(
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 availableMorePortfolio: Boolean,
)
data class SelectedNetwork(
internal data class SelectedNetwork(
val selectedNetwork: TokenMarketInfo.Network,
val cryptoCurrency: CryptoCurrency,
val availableMoreNetwork: Boolean,

View file

@ -1,6 +1,7 @@
package com.tangem.features.markets.portfolio.add.impl
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
@ -8,6 +9,7 @@ import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
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.SelectedPortfolio
import com.tangem.features.markets.portfolio.add.impl.model.AddTokenModel
@ -46,7 +48,7 @@ internal class AddTokenComponent @AssistedInject constructor(
interface Callbacks {
fun onChangeNetworkClick()
fun onChangePortfolioClick()
fun onTokenAdded()
fun onTokenAdded(status: CryptoCurrencyStatus)
}
@AssistedFactory

View file

@ -1,9 +1,13 @@
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.GetTokenMarketCryptoCurrency
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.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
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 javax.inject.Inject
class AvailableToAddDataConverter @Inject constructor(
internal class AvailableToAddDataConverter @Inject constructor(
private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase,
private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
) {
suspend fun convert(
balances: Map<UserWallet, PortfolioFetcher.PortfolioBalance>,
availableNetworks: Set<TokenMarketInfo.Network>,
marketParams: TokenMarketParams,
): AvailableToAddData {
fun AccountStatus.getAvailableToAddAccount(): AvailableToAddAccount {
val availableToAddNetworks = availableNetworks
.filterTo(mutableSetOf()) { network -> !this.isAlreadyAddedNetworks(network) }
suspend fun AccountStatus.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount {
val addedNetworks = availableNetworks
.mapNotNull { createCryptoCurrency(wallet, it, marketParams) }
.mapNotNull { getAccountCurrencyStatusUseCase.invokeSync(wallet.walletId, it) }
.mapNotNull { it.getOrNull()?.status?.currency?.network }
.toSet()
return AvailableToAddAccount(
account = this,
availableNetworks = availableNetworks,
availableToAddNetworks = availableToAddNetworks,
addedNetworks = addedNetworks,
)
}
fun UserWallet.getAvailableToAddWallet(balance: PortfolioFetcher.PortfolioBalance): AvailableToAddWallet {
val filteredNetworks = this.filteredAvailableNetworks(availableNetworks)
suspend fun getAvailableToAddWallet(
entry: Map.Entry<UserWallet, PortfolioFetcher.PortfolioBalance>,
): AvailableToAddWallet {
val (wallet, balance) = entry
val filteredNetworks = wallet.filteredAvailableNetworks(availableNetworks)
val accounts = balance.accountsBalance.accountStatuses
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() }
.toMap()
return AvailableToAddWallet(
userWallet = this,
userWallet = wallet,
accounts = accounts,
availableNetworks = filteredNetworks,
availableToAddAccounts = availableToAddAccounts,
@ -46,8 +59,9 @@ class AvailableToAddDataConverter @Inject constructor(
}
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet> = balances
.map { (wallet, balance) ->
val availableToAddWallet = wallet.getAvailableToAddWallet(balance)
.map {
val (wallet, balance) = it
val availableToAddWallet = getAvailableToAddWallet(it)
wallet.walletId to availableToAddWallet
}
.filter { (_, wallet) -> wallet.availableToAddAccounts.isNotEmpty() }
@ -64,8 +78,13 @@ class AvailableToAddDataConverter @Inject constructor(
networks = networks,
)
private fun AccountStatus.isAlreadyAddedNetworks(network: TokenMarketInfo.Network): Boolean = when (this) {
is AccountStatus.CryptoPortfolio -> this.tokenList.flattenCurrencies()
.any { status -> network.networkId == status.currency.network.backendId }
}
private suspend fun createCryptoCurrency(
userWallet: UserWallet,
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.model.Model
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.features.markets.portfolio.add.api.SelectedNetwork
import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio
@ -20,13 +21,15 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
@ModelScoped
@Suppress("LongParameterList")
internal class AddTokenModel @Inject constructor(
paramsContainer: ParamsContainer,
private val saveMarketTokensUseCase: SaveMarketTokensUseCase,
private val uiBuilder: AddTokenUiBuilder,
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val saveCryptoCurrenciesUseCase: SaveCryptoCurrenciesUseCase,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
) : Model() {
private val params = paramsContainer.require<AddTokenComponent.Params>()
@ -37,13 +40,10 @@ internal class AddTokenModel @Inject constructor(
field = MutableStateFlow(value = null)
init {
channelFlow {
val selectedNetwork = params.selectedNetwork.stateIn(this)
val selectedPortfolio = params.selectedPortfolio.stateIn(this)
combine(
flow = selectedNetwork,
flow2 = selectedPortfolio,
) { selectedNetwork, selectedPortfolio ->
combine(
flow = params.selectedNetwork.distinctUntilChanged(),
flow2 = params.selectedPortfolio.distinctUntilChanged(),
transform = { selectedNetwork, selectedPortfolio ->
addTokenJob.cancel()
val isTangemIconVisible = needColdWalletInteraction(selectedNetwork, selectedPortfolio)
uiBuilder.updateContent(
@ -52,29 +52,35 @@ internal class AddTokenModel @Inject constructor(
isTangemIconVisible = isTangemIconVisible,
onConfirmClick = { onAddClick(selectedNetwork, selectedPortfolio).saveIn(addTokenJob) },
)
}.collect { newUI -> send(newUI) }
}
.flowOn(dispatchers.default)
},
)
.onEach { newUI -> uiState.value = newUI }
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
private fun onAddClick(selectedNetwork: SelectedNetwork, selectedPortfolio: SelectedPortfolio) = modelScope.launch {
val um = uiState.value ?: return@launch
uiState.value = um.toggleProgress(true)
val blockchainNames = listOf(selectedNetwork.selectedNetwork)
.mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name }
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames))
// todo account
saveMarketTokensUseCase(
userWalletId = selectedPortfolio.userWallet.walletId,
tokenMarketParams = params.marketParams,
addedNetworks = setOf(selectedNetwork.selectedNetwork),
removedNetworks = setOf(),
)
params.callbacks.onTokenAdded()
uiState.value = um.toggleProgress(false)
}
private fun onAddClick(selectedNetwork: SelectedNetwork, selectedPortfolio: SelectedPortfolio) =
modelScope.launch(dispatchers.default) {
val um = uiState.value ?: return@launch
uiState.value = um.toggleProgress(true)
val blockchainNames = listOf(selectedNetwork.selectedNetwork)
.mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name }
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames))
val cryptoCurrency = selectedNetwork.cryptoCurrency
val accountId = selectedPortfolio.account.account.account.accountId
saveCryptoCurrenciesUseCase(
accountId = accountId,
add = listOf(cryptoCurrency),
remove = listOf(),
)
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(
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.impl.AddTokenComponent
import com.tangem.features.markets.portfolio.add.impl.ui.state.AddTokenUM
import javax.inject.Inject
@ModelScoped
internal class AddTokenUiBuilder(
internal class AddTokenUiBuilder @Inject constructor(
paramsContainer: ParamsContainer,
) {
private val params = paramsContainer.require<AddTokenComponent.Params>()
@ -41,9 +42,10 @@ internal class AddTokenUiBuilder(
portfolioName = stringReference(selectedPortfolio.userWallet.name)
}
true -> {
portfolioName = selectedPortfolio.account.account.accountName.toUM().value
accountIcon = when (selectedPortfolio.account) {
is AccountStatus.CryptoPortfolio -> selectedPortfolio.account.account.icon.toUM()
val accountStatus = selectedPortfolio.account.account
portfolioName = accountStatus.account.accountName.toUM().value
accountIcon = when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.account.icon.toUM()
}
}
}
@ -61,8 +63,11 @@ internal class AddTokenUiBuilder(
isTangemIconVisible: Boolean,
onConfirmClick: () -> Unit,
): 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(
isEnabled = true,
isEnabled = isAvailableNetwork,
showProgress = false,
isTangemIconVisible = isTangemIconVisible,
text = resourceReference(R.string.common_add),