Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-06 12:26:33 +04:00
parent 7a197ec6c9
commit c662136303
26 changed files with 906 additions and 346 deletions

View file

@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.account
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier
import com.tangem.domain.account.status.utils.ExpandedAccountsHolder
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import javax.inject.Inject

View file

@ -1,112 +0,0 @@
package com.tangem.feature.wallet.presentation.account
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.account.models.AccountExpandedState
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.repository.AccountsExpandedRepository
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@ModelScoped
internal class ExpandedAccountsHolder @Inject constructor(
private val singleAccountListSupplier: SingleAccountListSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val accountsExpandedRepository: AccountsExpandedRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
private val actionChannel = MutableSharedFlow<Pair<AccountId, Boolean>>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
fun expandedAccounts(userWallet: UserWallet): Flow<Set<AccountId>> = channelFlow {
val walletId = userWallet.walletId
val storedState = accountsExpandedRepository.expandedAccounts
.map { it[walletId].orEmpty() }
.stateIn(this)
val isAccountsMode = isAccountsModeEnabledUseCase.invoke()
.stateIn(this)
val initExpandedState = storedState.value
.mapNotNull { it.takeIf { state -> state.isExpanded }?.accountId }
.toSet()
// main state holder
val expandedAccounts = MutableStateFlow(initExpandedState)
var debounceJob: Job? = null
actionChannel
.filter { (accountId, _) -> accountId.userWalletId == walletId }
.filter { debounceJob?.isActive != true }
.onEach { (accountId, isExpand) ->
debounceJob = launch { delay(DEBOUNCE_MILLIS) }
val newState = AccountExpandedState(accountId, isExpand)
launch { accountsExpandedRepository.update(newState) }
if (isExpand) {
expandedAccounts.update { it.plus(accountId) }
} else {
expandedAccounts.update { it.minus(accountId) }
}
}
.launchIn(this)
walletAccounts(walletId).onEach { accountList ->
if (!isAccountsModeEnabledUseCase.invokeSync()) {
accountsExpandedRepository.clearStore()
expandedAccounts.update { setOf() }
return@onEach
}
val idsSet = accountList.accounts.mapTo(mutableSetOf()) { it.accountId }
accountsExpandedRepository.syncStore(walletId, idsSet)
val isSingleAccount = accountList.accounts.size == 1
val storedMainAccountState = storedState.value
.find { it.accountId == accountList.mainAccount.accountId }
if (isSingleAccount && storedMainAccountState == null) {
// force expand for single and not stored account
expandedAccounts.update { setOf(accountList.mainAccount.accountId) }
}
}.launchIn(this)
combine(
flow = expandedAccounts,
flow2 = isAccountsMode,
transform = { expanded, isAccountMode ->
if (isAccountMode) {
channel.send(expanded)
} else {
channel.send(setOf())
}
},
).collect()
}
.flowOn(dispatchers.default)
.distinctUntilChanged()
fun expandAccount(accountId: AccountId) {
actionChannel.tryEmit(accountId to true)
}
fun collapseAccount(accountId: AccountId) {
actionChannel.tryEmit(accountId to false)
}
private fun walletAccounts(walletId: UserWalletId): Flow<AccountList> = singleAccountListSupplier(walletId)
companion object {
private const val DEBOUNCE_MILLIS = 200L
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.common.ui.tokens.TokenConverterParams
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency

View file

@ -1,19 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.tokenlist.TokenList
sealed interface TokenConverterParams {
/** Wallet mode; list of tokens for main account */
data class Wallet(
val accountId: AccountId,
val tokenList: TokenList,
) : TokenConverterParams
/** Account mode; list of accounts */
data class Account(
val accountList: AccountStatusList,
val expandedAccounts: Set<AccountId>,
) : TokenConverterParams
}

View file

@ -7,7 +7,6 @@ import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.common.util.cardTypesResolver
@ -25,11 +24,10 @@ import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams
import com.tangem.common.ui.tokens.TokenConverterParams
import com.tangem.common.ui.tokens.TokenItemGrouping.toGroupedItems
import com.tangem.common.ui.tokens.TokenItemGrouping.toUngroupedItems
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.OrganizeTokensButtonConfig as WalletOrganizeTokensButtonConfig
@ -92,7 +90,7 @@ internal class TokenListStateConverter(
return when (params) {
is TokenConverterParams.Account -> convertAccountList(params)
is TokenConverterParams.Wallet -> convertTokenList(
tokenConverter = tokenStatusConverter(params.accountId),
tokenConverter = tokenStatusConverter(params.mainAccount.accountId),
tokenList = params.tokenList,
)
}
@ -102,11 +100,11 @@ internal class TokenListStateConverter(
when (tokenList) {
is TokenList.Empty -> WalletTokensListState.Empty
is TokenList.GroupedByNetwork -> WalletTokensListState.ContentState.Content(
items = tokenList.toGroupedItems(tokenConverter),
items = tokenList.toGroupedItems(tokenConverter).toPersistentList(),
organizeTokensButtonConfig = getOrganizeTokensButtonState(tokenList = tokenList),
)
is TokenList.Ungrouped -> WalletTokensListState.ContentState.Content(
items = tokenList.toUngroupedItems(tokenConverter),
items = tokenList.toUngroupedItems(tokenConverter).toPersistentList(),
organizeTokensButtonConfig = getOrganizeTokensButtonState(tokenList = tokenList),
)
}
@ -167,51 +165,6 @@ internal class TokenListStateConverter(
)
}
private fun TokenList.GroupedByNetwork.toGroupedItems(
tokenConverter: TokenItemStateConverter,
): PersistentList<TokensListItemUM> {
return groups.fold(initial = persistentListOf()) { acc, group ->
acc.mutate { it.addGroup(tokenConverter, group) }
}
}
private fun TokenList.Ungrouped.toUngroupedItems(
tokenConverter: TokenItemStateConverter,
): PersistentList<TokensListItemUM> {
return currencies.fold(initial = persistentListOf()) { acc, token ->
acc.mutate { it.addToken(tokenConverter, token) }
}
}
private fun MutableList<TokensListItemUM>.addGroup(
tokenConverter: TokenItemStateConverter,
group: NetworkGroup,
): List<TokensListItemUM> {
val groupTitle = TokensListItemUM.GroupTitle(
id = group.network.hashCode(),
text = resourceReference(
id = R.string.wallet_network_group_title,
formatArgs = wrappedList(group.network.name),
),
)
add(groupTitle)
group.currencies.forEach { token -> addToken(tokenConverter, token) }
return this
}
private fun MutableList<TokensListItemUM>.addToken(
tokenConverter: TokenItemStateConverter,
token: CryptoCurrencyStatus,
): List<TokensListItemUM> {
val tokenItemState = tokenConverter.convert(token)
add(TokensListItemUM.Token(tokenItemState))
return this
}
private fun getOrganizeTokensButtonState(tokenList: TokenList): WalletOrganizeTokensButtonConfig? {
val currenciesSize = when (tokenList) {
TokenList.Empty -> return null

View file

@ -4,7 +4,7 @@ import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.yieldSupplyKey
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams
import com.tangem.common.ui.tokens.TokenConverterParams
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.converter.Converter
import java.math.BigDecimal

View file

@ -16,7 +16,8 @@ import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListErrorTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams
import com.tangem.common.ui.tokens.TokenConverterParams
import com.tangem.domain.models.account.AccountStatus
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
@ -65,7 +66,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
singleAccountTransform(
maybeTokenList = maybeTokenList,
appCurrency = appCurrency,
accountId = mainAccount.accountId,
mainAccount = mainAccount,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
@ -111,7 +112,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
private fun singleAccountTransform(
maybeTokenList: Lce<TokenListError, TokenList>,
appCurrency: AppCurrency,
accountId: AccountId,
mainAccount: AccountStatus.CryptoPortfolio,
yieldSupplyApyMap: Map<String, BigDecimal> = emptyMap(),
stakingAvailabilityMap: Map<CryptoCurrency, StakingAvailability> = emptyMap(),
shouldShowMainPromo: Boolean,
@ -141,7 +142,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
)
updateContent(
params = TokenConverterParams.Wallet(accountId, tokenList),
params = TokenConverterParams.Wallet(mainAccount, tokenList),
appCurrency = appCurrency,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,

View file

@ -1,9 +1,11 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.PortfolioId
import com.tangem.common.ui.tokens.TokenConverterParams
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.Account.CryptoPortfolio.Companion.createMainAccount
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
@ -11,19 +13,25 @@ import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams
import org.junit.Test
import java.math.BigDecimal
class YieldSupplyPromoBannerConverterTest {
private val account
get() = AccountStatus.CryptoPortfolio(
tokenList = TokenList.Empty,
priceChangeLce = Unit.lceError(),
account = createMainAccount(UserWalletId("00"))
)
@Test
fun `GIVEN promo disabled WHEN convert THEN return null`() {
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xABCDEF")
val status = createLoadedStatus(token = token, amount = BigDecimal.ONE, isYieldActive = false)
val tokenList = ungroupedTokenList(status)
val params = TokenConverterParams.Wallet(
accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")),
mainAccount = account,
tokenList = tokenList,
)
val converter = YieldSupplyPromoBannerConverter(
@ -41,7 +49,7 @@ class YieldSupplyPromoBannerConverterTest {
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xA1")
val status = createLoadedStatus(token = token, amount = BigDecimal("2.0"), isYieldActive = false)
val params = TokenConverterParams.Wallet(
accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")),
mainAccount = account,
tokenList = ungroupedTokenList(status),
)
val converter = YieldSupplyPromoBannerConverter(
@ -59,7 +67,7 @@ class YieldSupplyPromoBannerConverterTest {
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xAA")
val statusActive = createLoadedStatus(token = token, amount = BigDecimal("5"), isYieldActive = true)
val params = TokenConverterParams.Wallet(
accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")),
mainAccount = account,
tokenList = ungroupedTokenList(statusActive),
)
val converter = YieldSupplyPromoBannerConverter(
@ -87,7 +95,7 @@ class YieldSupplyPromoBannerConverterTest {
)
val params = TokenConverterParams.Wallet(
accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")),
mainAccount = account,
tokenList = ungroupedTokenList(statusSmall, statusBig),
)
val converter = YieldSupplyPromoBannerConverter(
@ -110,7 +118,7 @@ class YieldSupplyPromoBannerConverterTest {
val apyMap = mapOf(mismatchedKey to BigDecimal("0.07"))
val params = TokenConverterParams.Wallet(
accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")),
mainAccount = account,
tokenList = ungroupedTokenList(status),
)
val converter = YieldSupplyPromoBannerConverter(
@ -128,7 +136,7 @@ class YieldSupplyPromoBannerConverterTest {
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xCUSTOM")
val status = createCustomStatus(token = token, amount = BigDecimal("5.0"), isYieldActive = false)
val params = TokenConverterParams.Wallet(
accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")),
mainAccount = account,
tokenList = ungroupedTokenList(status),
)
val converter = YieldSupplyPromoBannerConverter(