Updated on 2026-08-14
This commit is contained in:
parent
6ad742d678
commit
9101098d72
6 changed files with 116 additions and 34 deletions
|
|
@ -101,8 +101,9 @@ internal class ChooseTokenListItemConverter(
|
|||
private fun AccountStatus.CryptoPortfolio.toPortfolioItem(
|
||||
params: TokenConverterParams.Account,
|
||||
): TokensListItemUM.Portfolio {
|
||||
val tokenList: TokenList = this.tokenList
|
||||
val account: Account.CryptoPortfolio = this.account
|
||||
val displayedStatus = filterForDisplay()
|
||||
val account: Account.CryptoPortfolio = displayedStatus.account
|
||||
val displayedTokenList: TokenList = displayedStatus.tokenList
|
||||
val isExpanded = isSearchingState || params.expandedAccounts.contains(account.accountId)
|
||||
val onItemClick: (Account.CryptoPortfolio) -> Unit = { clickedAccount ->
|
||||
onAccountItemClick(clickedAccount, isExpanded)
|
||||
|
|
@ -116,10 +117,9 @@ internal class ChooseTokenListItemConverter(
|
|||
fiatAmountStateProvider = { fiatBalance -> fiatAmountStateProvider(fiatBalance, isExpanded) },
|
||||
subtitle2StateProvider = { _ -> null },
|
||||
)
|
||||
val accountItem = converter.convert(tokenList.totalFiatBalance)
|
||||
val tokenConverter = tokenStatusConverter(this)
|
||||
val tokensListState = convertTokenList(tokenConverter, tokenList, this)
|
||||
val items = tokensListState.tokensList
|
||||
val accountItem = converter.convert(displayedTokenList.totalFiatBalance)
|
||||
val items = displayedTokenList.toUmData(tokenStatusConverter(this)).tokensList
|
||||
|
||||
return TokensListPortfolioItemConverter(
|
||||
tokenItemUM = accountItem,
|
||||
isExpanded = isExpanded,
|
||||
|
|
@ -128,22 +128,30 @@ internal class ChooseTokenListItemConverter(
|
|||
).convert(Unit)
|
||||
}
|
||||
|
||||
private fun AccountStatus.CryptoPortfolio.filterForDisplay(): AccountStatus.CryptoPortfolio {
|
||||
val filteredTokenList = filterTokenList(tokenList, this)
|
||||
return copy(
|
||||
account = account.copy(cryptoCurrencies = filteredTokenList.flattenCurrencies().map { it.currency }),
|
||||
tokenList = filteredTokenList,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertTokenList(
|
||||
tokenConverter: TokenItemStateConverter,
|
||||
tokenListParam: TokenList,
|
||||
account: AccountStatus.CryptoPortfolio,
|
||||
): TokenListUMData {
|
||||
return when (val tokenList = filterTokenList(tokenListParam, account)) {
|
||||
is TokenList.Empty -> TokenListUMData.EmptyList
|
||||
is TokenList.GroupedByNetwork -> TokenListUMData.TokenList(
|
||||
tokensList = tokenList.toGroupedItems(tokenConverter).toPersistentList(),
|
||||
totalTokensCount = tokenList.flattenCurrencies().size,
|
||||
)
|
||||
is TokenList.Ungrouped -> TokenListUMData.TokenList(
|
||||
tokensList = tokenList.toUngroupedItems(tokenConverter).toPersistentList(),
|
||||
totalTokensCount = tokenList.flattenCurrencies().size,
|
||||
)
|
||||
}
|
||||
): TokenListUMData = filterTokenList(tokenListParam, account).toUmData(tokenConverter)
|
||||
|
||||
private fun TokenList.toUmData(tokenConverter: TokenItemStateConverter): TokenListUMData = when (this) {
|
||||
TokenList.Empty -> TokenListUMData.EmptyList
|
||||
is TokenList.GroupedByNetwork -> TokenListUMData.TokenList(
|
||||
tokensList = toGroupedItems(tokenConverter).toPersistentList(),
|
||||
totalTokensCount = flattenCurrencies().size,
|
||||
)
|
||||
is TokenList.Ungrouped -> TokenListUMData.TokenList(
|
||||
tokensList = toUngroupedItems(tokenConverter).toPersistentList(),
|
||||
totalTokensCount = flattenCurrencies().size,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<CryptoCurrencyStatus>.filterCurrencies(account: AccountStatus): List<CryptoCurrencyStatus> =
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.features.commonfeatures.api.R
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.*
|
||||
|
|
@ -20,6 +22,7 @@ import com.tangem.features.commonfeatures.impl.choosetoken.ui.ChooseTokenFullUM
|
|||
import com.tangem.features.commonfeatures.impl.choosetoken.ui.ChooseTokenInitialUM
|
||||
import com.tangem.features.commonfeatures.impl.choosetoken.ui.state.ChooserBlockUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -32,6 +35,7 @@ internal class ChooseTokenModel @Inject constructor(
|
|||
marketBlockDelegateFactory: MarketBlockDelegate.Factory,
|
||||
predefinedTokensBlockDelegateFactory: PredefinedTokensBlockDelegate.Factory,
|
||||
addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -62,6 +66,13 @@ internal class ChooseTokenModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
/** Tokens the user already holds in the selected wallet — subtracted from the predefined "Other eligible" block. */
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val portfolioTokenKeysFlow: Flow<Set<Pair<String, String>>> = bridge.selectedWalletFlow
|
||||
.flatMapLatest { wallet -> singleAccountStatusListSupplier(wallet.walletId) }
|
||||
.map { accountStatusList -> accountStatusList.toTokenKeys() }
|
||||
.onStart { emit(emptySet()) }
|
||||
|
||||
private val predefinedTokensBlockDelegate: PredefinedTokensBlockDelegate by lazy {
|
||||
val block = bridge.settings.chooserBlock as ChooserBlock.Predefined
|
||||
predefinedTokensBlockDelegateFactory.create(
|
||||
|
|
@ -71,6 +82,7 @@ internal class ChooseTokenModel @Inject constructor(
|
|||
addToPortfolioSlot = bottomSheetNavigation,
|
||||
modelScope = modelScope,
|
||||
tokenFilter = bridge.tokenFilter,
|
||||
portfolioTokenKeys = portfolioTokenKeysFlow,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -124,6 +136,12 @@ internal class ChooseTokenModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun AccountStatusList.toTokenKeys(): Set<Pair<String, String>> =
|
||||
flattenCurrencies().mapNotNullTo(hashSetOf()) { status ->
|
||||
val rawId = status.currency.id.rawCurrencyId?.value ?: return@mapNotNullTo null
|
||||
rawId to status.currency.network.rawId
|
||||
}
|
||||
|
||||
fun onBackClicked() {
|
||||
bridge.onClose()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ internal class PredefinedTokensBlockDelegate @AssistedInject constructor(
|
|||
@Assisted private val addToPortfolioSlot: SlotNavigation<AddToPortfolioRoute>,
|
||||
@Assisted private val modelScope: CoroutineScope,
|
||||
@Assisted private val tokenFilter: MutableStateFlow<(AccountStatus, CryptoCurrencyStatus) -> Boolean>,
|
||||
@Assisted private val portfolioTokenKeys: Flow<Set<Pair<String, String>>>,
|
||||
) {
|
||||
|
||||
init {
|
||||
|
|
@ -44,8 +45,13 @@ internal class PredefinedTokensBlockDelegate @AssistedInject constructor(
|
|||
val stateFlow: Flow<PredefinedTokensUM?> = combine(
|
||||
predefinedTokens,
|
||||
searchQueryState,
|
||||
) { tokens, query ->
|
||||
val filtered = tokens.filter { it.hasValidNetwork() && it.matchesQuery(query.value) }
|
||||
portfolioTokenKeys,
|
||||
) { tokens, query, portfolioKeys ->
|
||||
val filtered = tokens.filter { token ->
|
||||
token.hasValidNetwork() &&
|
||||
token.matchesQuery(query.value) &&
|
||||
!portfolioKeys.contains(token.toKey())
|
||||
}
|
||||
if (filtered.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
|
|
@ -66,6 +72,9 @@ internal class PredefinedTokensBlockDelegate @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/** Identity of a predefined token as `(rawCurrencyId, networkId)` — matches the portfolio token keys. */
|
||||
private fun PredefinedTokenToAdd.toKey(): Pair<String, String> = token.id.value to network.networkId
|
||||
|
||||
private fun PredefinedTokenToAdd.hasValidNetwork(): Boolean =
|
||||
network.networkId.isNotBlank() && network.decimalCount != null
|
||||
|
||||
|
|
@ -104,6 +113,7 @@ internal class PredefinedTokensBlockDelegate @AssistedInject constructor(
|
|||
addToPortfolioSlot: SlotNavigation<AddToPortfolioRoute>,
|
||||
modelScope: CoroutineScope,
|
||||
tokenFilter: MutableStateFlow<(AccountStatus, CryptoCurrencyStatus) -> Boolean>,
|
||||
portfolioTokenKeys: Flow<Set<Pair<String, String>>>,
|
||||
): PredefinedTokensBlockDelegate
|
||||
}
|
||||
}
|
||||
|
|
@ -125,6 +125,41 @@ internal class PredefinedTokensBlockDelegateTest {
|
|||
assertThat(actual).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN predefined token already in portfolio WHEN state emitted THEN it is excluded`() = runTest {
|
||||
// Arrange
|
||||
val tokens = listOf(
|
||||
createPredefinedToken(id = "usd-coin", symbol = "USDC", networkId = ETHEREUM_NETWORK_ID),
|
||||
createPredefinedToken(id = "tether", symbol = "USDT", networkId = ETHEREUM_NETWORK_ID),
|
||||
)
|
||||
val delegate = createDelegate(
|
||||
predefinedTokens = MutableStateFlow(tokens),
|
||||
portfolioTokenKeys = MutableStateFlow(setOf("usd-coin" to ETHEREUM_NETWORK_ID)),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = lastState(delegate)
|
||||
|
||||
// Assert — usd-coin is already in the portfolio, so only tether stays in "Other eligible tokens"
|
||||
assertThat(actual?.items?.map { it.id }).containsExactly("tether_$ETHEREUM_NETWORK_ID")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN all predefined tokens already in portfolio WHEN state emitted THEN emits null`() = runTest {
|
||||
// Arrange
|
||||
val token = createPredefinedToken(id = "usd-coin", symbol = "USDC", networkId = ETHEREUM_NETWORK_ID)
|
||||
val delegate = createDelegate(
|
||||
predefinedTokens = MutableStateFlow(listOf(token)),
|
||||
portfolioTokenKeys = MutableStateFlow(setOf("usd-coin" to ETHEREUM_NETWORK_ID)),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = lastState(delegate)
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isNull()
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun filter(model: FilterModel) = runTest {
|
||||
|
|
@ -234,6 +269,7 @@ internal class PredefinedTokensBlockDelegateTest {
|
|||
searchQueryState: MutableStateFlow<SearchQuery> = MutableStateFlow(SearchQuery.Empty),
|
||||
tokenFilter: MutableStateFlow<(AccountStatus, CryptoCurrencyStatus) -> Boolean> =
|
||||
MutableStateFlow({ _, _ -> true }),
|
||||
portfolioTokenKeys: MutableStateFlow<Set<Pair<String, String>>> = MutableStateFlow(emptySet()),
|
||||
): PredefinedTokensBlockDelegate = PredefinedTokensBlockDelegate(
|
||||
predefinedTokens = predefinedTokens,
|
||||
searchQueryState = searchQueryState,
|
||||
|
|
@ -241,6 +277,7 @@ internal class PredefinedTokensBlockDelegateTest {
|
|||
addToPortfolioSlot = addToPortfolioSlot,
|
||||
modelScope = CoroutineScope(backgroundScope.coroutineContext + UnconfinedTestDispatcher(testScheduler)),
|
||||
tokenFilter = tokenFilter,
|
||||
portfolioTokenKeys = portfolioTokenKeys,
|
||||
)
|
||||
|
||||
private fun currency(rawId: String, networkId: String): CryptoCurrencyStatus =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue