Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-29 15:48:01 +03:00
commit 5f4318ca5d
39 changed files with 619 additions and 120 deletions

View file

@ -51,7 +51,17 @@ internal class PreviewCustomTokenSelectorComponent(
)
CurrencyNetworkUM(
networkId = n.id,
network = Network(
id = n.id,
backendId = n.id.value,
name = "",
currencySymbol = "",
derivationPath = Network.DerivationPath.Card(""),
isTestnet = false,
standardType = Network.StandardType.ERC20,
hasFiatFeeRate = false,
canHandleTokens = false,
),
name = "Network $index",
type = "N$index",
iconResId = R.drawable.ic_eth_16,

View file

@ -54,6 +54,7 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
isNextBatchLoading = true,
loadMore = { false },
saveChanges = {},
isSavingInProgress = false,
),
)
@ -134,7 +135,17 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex ->
CurrencyNetworkUM(
networkId = Network.ID(networkIndex.toString()),
network = Network(
id = Network.ID(networkIndex.toString()),
backendId = networkIndex.toString(),
name = "",
currencySymbol = "",
derivationPath = Network.DerivationPath.Card(""),
isTestnet = false,
standardType = Network.StandardType.ERC20,
hasFiatFeeRate = false,
canHandleTokens = false,
),
name = "NETWORK$networkIndex",
type = "N$networkIndex",
iconResId = R.drawable.ic_eth_16,

View file

@ -3,7 +3,7 @@ package com.tangem.features.managetokens.entity.item
import com.tangem.domain.tokens.model.Network
internal data class CurrencyNetworkUM(
val networkId: Network.ID,
val network: Network,
val name: String,
val type: String,
val iconResId: Int,
@ -12,5 +12,5 @@ internal data class CurrencyNetworkUM(
override val onSelectedStateChange: (Boolean) -> Unit,
) : SelectableItemUM {
override val id: String = networkId.value
override val id: String = network.id.value
}

View file

@ -36,6 +36,7 @@ internal sealed class ManageTokensUM {
override val loadMore: () -> Boolean,
val saveChanges: () -> Unit,
val hasChanges: Boolean,
val isSavingInProgress: Boolean,
) : ManageTokensUM()
fun copySealed(
@ -44,6 +45,7 @@ internal sealed class ManageTokensUM {
hasChanges: Boolean = this is ManageContent && this.hasChanges,
isInitialBatchLoading: Boolean = this.isInitialBatchLoading,
isNextBatchLoading: Boolean = this.isNextBatchLoading,
isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress,
): ManageTokensUM {
return when (this) {
is ManageContent -> copy(
@ -52,6 +54,7 @@ internal sealed class ManageTokensUM {
hasChanges = hasChanges,
isInitialBatchLoading = isInitialBatchLoading,
isNextBatchLoading = isNextBatchLoading,
isSavingInProgress = isSavingInProgress,
)
is ReadContent -> copy(
search = search,

View file

@ -12,6 +12,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.SaveManagedTokensUseCase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
@ -21,12 +22,14 @@ import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.list.ChangedCurrencies
import com.tangem.features.managetokens.utils.list.ManageTokensListManager
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ComponentScoped
@ -35,6 +38,7 @@ internal class ManageTokensModel @Inject constructor(
private val router: Router,
private val manageTokensListManager: ManageTokensListManager,
private val messageSender: UiMessageSender,
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
paramsContainer: ParamsContainer,
) : Model() {
@ -76,7 +80,7 @@ internal class ManageTokensModel @Inject constructor(
popBack = router::pop,
isInitialBatchLoading = true,
isNextBatchLoading = false,
items = getInitialItems(),
items = persistentListOf(),
topBar = ManageTokensTopBarUM.ReadContent(
title = resourceReference(R.string.common_search_tokens),
onBackButtonClick = router::pop,
@ -97,7 +101,7 @@ internal class ManageTokensModel @Inject constructor(
popBack = router::pop,
isInitialBatchLoading = true,
isNextBatchLoading = false,
items = getInitialItems(),
items = persistentListOf(),
topBar = ManageTokensTopBarUM.ManageContent(
title = resourceReference(id = R.string.main_manage_tokens),
onBackButtonClick = router::pop,
@ -116,6 +120,7 @@ internal class ManageTokensModel @Inject constructor(
hasChanges = false,
saveChanges = ::saveChanges,
loadMore = ::loadMoreItems,
isSavingInProgress = false,
)
}
@ -157,9 +162,16 @@ internal class ManageTokensModel @Inject constructor(
isNextBatchLoading = false,
)
}
is PaginationStatus.Paginating,
is PaginationStatus.EndOfPagination,
-> state.copySealed(
is PaginationStatus.Paginating -> {
(status.lastResult as? BatchFetchResult.Error)?.let { fetchError ->
Timber.e(fetchError.throwable)
}
state.copySealed(
isInitialBatchLoading = false,
isNextBatchLoading = false,
)
}
is PaginationStatus.EndOfPagination -> state.copySealed(
isInitialBatchLoading = false,
isNextBatchLoading = false,
)
@ -189,10 +201,6 @@ internal class ManageTokensModel @Inject constructor(
return true
}
private fun getInitialItems(): ImmutableList<CurrencyItemUM> {
return persistentListOf()
}
private fun navigateToAddCustomToken() {
params.userWalletId?.let {
bottomSheetNavigation.activate(BottomSheetConfig.AddCustomToken(it))
@ -200,7 +208,18 @@ internal class ManageTokensModel @Inject constructor(
}
private fun saveChanges() {
// TODO: [REDACTED_JIRA]
modelScope.launch {
state.update { state -> state.copySealed(isSavingInProgress = true) }
saveManagedTokensUseCase.invoke(
userWalletId = requireNotNull(params.userWalletId),
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,
currenciesToRemove = manageTokensListManager.currenciesToRemove.value,
).fold(
ifLeft = { Timber.e(it, "Failed to save changes") },
ifRight = { router.pop() },
)
state.update { state -> state.copySealed(isSavingInProgress = false) }
}
}
private fun searchCurrencies(query: String) {

View file

@ -102,6 +102,7 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
isVisible = state.hasChanges,
showProgress = state.isSavingInProgress,
onClick = state.saveChanges,
)
}
@ -133,7 +134,12 @@ private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, search: SearchBarUM
}
@Composable
private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
private fun SaveChangesButton(
isVisible: Boolean,
showProgress: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
modifier = modifier,
visible = isVisible,
@ -144,6 +150,7 @@ private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier:
PrimaryButtonIconEnd(
text = stringResource(id = R.string.common_save),
iconResId = R.drawable.ic_tangem_24,
showProgress = showProgress,
onClick = onClick,
)
}

View file

@ -5,54 +5,54 @@ import com.tangem.domain.tokens.model.Network
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
internal typealias ChangedCurrencies = Map<ManagedCryptoCurrency.ID, Set<Network.ID>>
internal typealias ChangedCurrencies = Map<ManagedCryptoCurrency.Token, Set<Network>>
internal class ChangedCurrenciesManager {
val currenciesToAdd: MutableStateFlow<ChangedCurrencies> = MutableStateFlow(emptyMap())
val currenciesToRemove: MutableStateFlow<ChangedCurrencies> = MutableStateFlow(emptyMap())
fun addCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) {
updateChangedItems(currencyId, networkId, currenciesToRemove, currenciesToAdd)
fun addCurrency(currency: ManagedCryptoCurrency.Token, network: Network) {
updateChangedItems(currency, network, currenciesToRemove, currenciesToAdd)
}
fun removeCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) {
updateChangedItems(currencyId, networkId, currenciesToAdd, currenciesToRemove)
fun removeCurrency(currency: ManagedCryptoCurrency.Token, network: Network) {
updateChangedItems(currency, network, currenciesToAdd, currenciesToRemove)
}
fun containsCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean {
return networkId in currenciesToAdd.value[currencyId].orEmpty() ||
networkId in currenciesToRemove.value[currencyId].orEmpty()
fun containsCurrency(currency: ManagedCryptoCurrency.Token, network: Network): Boolean {
return network in currenciesToAdd.value[currency].orEmpty() ||
network in currenciesToRemove.value[currency].orEmpty()
}
private fun updateChangedItems(
currencyId: ManagedCryptoCurrency.ID,
networkId: Network.ID,
currency: ManagedCryptoCurrency.Token,
network: Network,
removeFromIfPresent: MutableStateFlow<ChangedCurrencies>,
addToIfNotPresent: MutableStateFlow<ChangedCurrencies>,
) {
val present = removeFromIfPresent.value[currencyId].orEmpty()
val present = removeFromIfPresent.value[currency].orEmpty()
if (networkId in present) {
if (network in present) {
removeFromIfPresent.update { items ->
items.toMutableMap().apply {
val ids = present - networkId
val ids = present - network
if (ids.isEmpty()) {
remove(currencyId)
remove(currency)
} else {
set(currencyId, ids)
set(currency, ids)
}
}
}
} else {
addToIfNotPresent.update { items ->
val alreadyAdded = items[currencyId] ?: emptySet()
if (networkId in alreadyAdded) {
val alreadyAdded = items[currency] ?: emptySet()
if (network in alreadyAdded) {
return@update items
}
items + (currencyId to alreadyAdded + networkId)
items + (currency to alreadyAdded + network)
}
}
}

View file

@ -59,8 +59,8 @@ internal class ManageTokensListManager @Inject constructor(
scopeProvider = Provider { scope },
)
val currenciesToAdd: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToAdd
val currenciesToRemove: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToRemove
val currenciesToAdd: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToAdd.asStateFlow()
val currenciesToRemove: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToRemove.asStateFlow()
@OptIn(ExperimentalCoroutinesApi::class)
val paginationStatus: Flow<PaginationStatus<*>> = state
@ -148,32 +148,32 @@ internal class ManageTokensListManager @Inject constructor(
}
}
override fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) {
changedCurrenciesManager.addCurrency(currencyId, networkId)
override fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) {
changedCurrenciesManager.addCurrency(currency, network)
sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = true)
sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = true)
}
override fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) {
changedCurrenciesManager.removeCurrency(currencyId, networkId)
override fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) {
changedCurrenciesManager.removeCurrency(currency, network)
sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = false)
sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false)
}
override fun checkNeedToShowRemoveNetworkWarning(
currencyId: ManagedCryptoCurrency.ID,
networkId: Network.ID,
): Boolean = !changedCurrenciesManager.containsCurrency(currencyId, networkId)
currency: ManagedCryptoCurrency.Token,
network: Network,
): Boolean = !changedCurrenciesManager.containsCurrency(currency, network)
private fun sendSelectCurrencyAction(
batchKey: Int,
currencyId: ManagedCryptoCurrency.ID,
networkId: Network.ID,
network: Network,
isSelected: Boolean,
) {
val request = ManageTokensUpdateAction.AddCurrency(
currencyId = currencyId,
networkId = networkId,
network = network,
isSelected = isSelected,
)
val action = BatchAction.UpdateBatches(

View file

@ -6,11 +6,11 @@ import com.tangem.domain.wallets.models.UserWalletId
internal interface ManageTokensUiActions {
fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID)
fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network)
fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID)
fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network)
fun checkNeedToShowRemoveNetworkWarning(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean
fun checkNeedToShowRemoveNetworkWarning(currency: ManagedCryptoCurrency.Token, network: Network): Boolean
suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean
}

View file

@ -142,19 +142,19 @@ internal class ManageTokensUiManager(
if (currency !is ManagedCryptoCurrency.Token) return@launch
if (isSelected) {
actions.addCurrency(batchKey, currency.id, source.id)
actions.addCurrency(batchKey, currency, source.network)
} else {
if (actions.checkNeedToShowRemoveNetworkWarning(currency.id, source.id)) {
if (actions.checkNeedToShowRemoveNetworkWarning(currency, source.network)) {
showRemoveNetworkWarning(
currency = currency,
network = source.network,
isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main,
onConfirm = {
actions.removeCurrency(batchKey, currency.id, source.id)
actions.removeCurrency(batchKey, currency, source.network)
},
)
} else {
actions.removeCurrency(batchKey, currency.id, source.id)
actions.removeCurrency(batchKey, currency, source.network)
}
}
}

View file

@ -16,7 +16,7 @@ internal fun ManagedCryptoCurrency.Token.toUiNetworksModel(
NetworksUM.Expanded(
networks = availableNetworks.map {
it.toUiModel(
isSelected = it.id in addedIn,
isSelected = it.network in addedIn,
isEditable = isItemsEditable,
onSelectedStateChange = onSelectedStateChange,
)
@ -33,7 +33,7 @@ private fun SourceNetwork.toUiModel(
onSelectedStateChange: (SourceNetwork, Boolean) -> Unit,
): CurrencyNetworkUM {
return CurrencyNetworkUM(
networkId = id,
network = network,
name = network.name.uppercase(),
iconResId = id.getIconRes(isColored = isSelected || !isEditable),
isSelected = isSelected || !isEditable,

View file

@ -55,10 +55,10 @@ private fun CurrencyItemUM.Basic.updateNetworks(currency: ManagedCryptoCurrency.
is NetworksUM.Collapsed -> networks
is NetworksUM.Expanded -> networks.copy(
networks = networks.networks.map { network ->
val isSelected = network.networkId in currency.addedIn
val isSelected = network.network in currency.addedIn
network.copy(
iconResId = network.networkId.getIconRes(isSelected),
iconResId = network.network.id.getIconRes(isSelected),
isSelected = isSelected,
)
}.toImmutableList(),

View file

@ -8,7 +8,7 @@ import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
internal fun CurrencyNetworkUM.select(isSelected: Boolean): CurrencyNetworkUM {
return copy(
iconResId = networkId.getIconRes(isSelected),
iconResId = network.id.getIconRes(isSelected),
isSelected = isSelected,
)
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.markets.portfolio.impl.loader
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
/**
* Portfolio data. Combined data from all flows that required to setup portfolio
*
* @property walletsWithCurrencyStatuses wallets with crypto currency statuses
* @property appCurrency app currency
* @property isBalanceHidden flag that indicates if balance should be hidden
* @property walletsWithBalance wallets with total balance
*
[REDACTED_AUTHOR]
*/
internal data class PortfolioData(
val walletsWithCurrencyStatuses: Map<UserWallet, List<CryptoCurrencyStatus>>,
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
val walletsWithBalance: Map<UserWalletId, Lce<TokenListError, TotalFiatBalance>>,
)

View file

@ -0,0 +1,105 @@
package com.tangem.features.markets.portfolio.impl.loader
import arrow.core.getOrElse
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import timber.log.Timber
import javax.inject.Inject
/**
* Loader of portfolio data
*
* @property getAllWalletsCryptoCurrencyStatusesUseCase use case for getting all wallets crypto currency statuses
* @property getSelectedAppCurrencyUseCase use case for getting selected app currency
* @property getBalanceHidingSettingsUseCase use case for getting balance hiding settings
* @property getWalletTotalBalanceUseCase use case for getting wallet total balance
*
[REDACTED_AUTHOR]
*/
internal class PortfolioDataLoader @Inject constructor(
private val getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
) {
/** Load data by [currencyRawId] */
@OptIn(ExperimentalCoroutinesApi::class)
fun load(currencyRawId: String): Flow<PortfolioData> {
return combine(
flow = getAllWalletsCryptoCurrencyStatusesFlow(currencyRawId = currencyRawId),
flow2 = getSelectedAppCurrencyFlow(),
flow3 = getBalanceHidingSettingsFlow(),
) { walletsWithCurrencyStatuses, appCurrency, isBalanceHidden ->
PortfolioData(
walletsWithCurrencyStatuses = walletsWithCurrencyStatuses,
appCurrency = appCurrency,
isBalanceHidden = isBalanceHidden,
walletsWithBalance = emptyMap(),
)
}
// setup balances for wallets from walletsWithCurrencyStatuses
.flatMapLatest { portfolioData ->
getWalletsWithTotalBalanceFlow(
ids = portfolioData.walletsWithCurrencyStatuses.keys.map(UserWallet::walletId),
)
.map { portfolioData.copy(walletsWithBalance = it) }
}
}
private fun getAllWalletsCryptoCurrencyStatusesFlow(
currencyRawId: String,
): Flow<Map<UserWallet, List<CryptoCurrencyStatus>>> {
return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId)
.distinctUntilChanged()
.map { walletsWithMaybeStatuses ->
walletsWithMaybeStatuses.mapValues { entry ->
entry.value.mapNotNull { it.getOrNull() }
}
}
}
private fun getSelectedAppCurrencyFlow(): Flow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map {
it.getOrElse { e ->
Timber.e("Failed to load app currency: $e")
AppCurrency.Default
}
}
.distinctUntilChanged()
}
private fun getBalanceHidingSettingsFlow(): Flow<Boolean> {
return getBalanceHidingSettingsUseCase()
.map { it.isBalanceHidden }
.distinctUntilChanged()
}
private fun getWalletsWithTotalBalanceFlow(
ids: List<UserWalletId>,
): Flow<Map<UserWalletId, Lce<TokenListError, TotalFiatBalance>>> {
return combine(
flows = ids
.map { userWalletId ->
getWalletTotalBalanceUseCase(userWalletId)
.map { userWalletId to it }
.distinctUntilChanged()
},
transform = { it.toMap() },
)
.distinctUntilChanged()
.onEmpty { ids.associateWith { Lce.Loading<TotalFiatBalance>(partialContent = null) } }
}
}

View file

@ -109,7 +109,6 @@ private fun Preview() {
),
),
onBack = {},
onWalletSelected = {},
),
),
)
@ -135,7 +134,6 @@ private fun PreviewContent() {
),
),
onBack = {},
onWalletSelected = {},
),
)
}

View file

@ -6,6 +6,5 @@ import kotlinx.collections.immutable.ImmutableList
internal data class WalletSelectorBSContentUM(
val userWallets: ImmutableList<UserWalletItemUM>,
val onWalletSelected: (UserWalletItemUM) -> Unit,
val onBack: () -> Unit,
) : TangemBottomSheetConfigContent