Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-16 13:46:34 +04:00
parent 6badbf18b5
commit 70aa023245
12 changed files with 270 additions and 184 deletions

View file

@ -8,7 +8,7 @@ import com.tangem.domain.models.tokenlist.TokenList
sealed interface TokenConverterParams {
/** Wallet mode; list of tokens for main account */
data class Wallet(
val mainAccount: AccountStatus,
val mainAccount: AccountStatus.CryptoPortfolio,
val tokenList: TokenList,
) : TokenConverterParams

View file

@ -12,7 +12,6 @@ import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState
import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.context.childByContext
@ -22,7 +21,6 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload
import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent
import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent
import com.tangem.feature.swap.model.SwapModel
@ -56,10 +54,6 @@ internal class DefaultSwapComponent @AssistedInject constructor(
context = child("chooseTokenComponent"),
params = ChooseTokenComponent.Params(
bridge = model.chooseTokenBridge,
settings = ChooseTokenComponent.Settings.SwapTo,
analyticsPayload = setOf(
ChooseTokenAnalyticsPayload.ScreensSources(ScreensSources.Swap.value),
),
),
)
}

View file

@ -0,0 +1,126 @@
package com.tangem.feature.swap.choosetoken.api
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.account.Account
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.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup
import com.tangem.feature.swap.models.TokenListUMData
import com.tangem.feature.swap.presentation.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
// todo swap move to common-features module
interface ChooseTokenBridge : ChooseTokenBridgeLegacy, ChooseTokenBridgeInternal {
val onCurrencyChosen: Channel<ChooseTokenResult>
val onClose: Channel<Unit>
/**
* for some Feature specific tokens filtering
*/
val tokenFilter: MutableStateFlow<(AccountStatus.CryptoPortfolio, CryptoCurrencyStatus) -> Boolean>
data class Settings(
val title: TextReference,
val isShowMarketBlock: Boolean,
) {
companion object {
val SwapFrom = Settings(
title = resourceReference(R.string.swapping_from_title),
isShowMarketBlock = false,
)
val SwapTo = Settings(
title = resourceReference(R.string.swapping_to_title),
isShowMarketBlock = true,
)
}
}
interface Factory {
fun create(
modelScope: CoroutineScope,
settings: Settings,
analyticsPayload: Set<ChooseTokenAnalyticsPayload> = emptySet(),
): ChooseTokenBridge
}
}
/**
* primary for internal impl usage, but you can also use it externally
*/
interface ChooseTokenBridgeInternal {
val settings: ChooseTokenBridge.Settings
val analyticsPayload: Set<ChooseTokenAnalyticsPayload>
val searchQueryState: StateFlow<SearchQuery>
val portfolioListBlock: Flow<Map<UserWalletId, TokenListUMData>>
fun onSearchQuery(query: SearchQuery)
fun onSearchQuery(query: String) = onSearchQuery(SearchQuery(query))
fun onClose()
fun onCurrencyChosen(result: ChooseTokenResult)
@JvmInline
value class SearchQuery(val value: String) {
companion object {
val Empty = SearchQuery("")
val SearchQuery.isSearchingState: Boolean get() = this.value.isNotBlank()
val StateFlow<SearchQuery>.isSearchingState: Boolean get() = this.value.isSearchingState
}
}
}
// todo swap legacy api, remove
interface ChooseTokenBridgeLegacy : ChooseTokenBridgeInternal {
val onTokenSelected: Channel<ChooseTokenResultOld>
val onNewTokenAdded: Channel<Pair<CryptoCurrency, ChooseTokenAnalyticsPayload.IsSearched>>
val currenciesGroup: Flow<CurrenciesGroup>
fun onTokenSelected(result: ChooseTokenResultOld) {
onTokenSelected.trySend(result)
onSearchQuery(SearchQuery.Empty)
}
fun onNewTokenAdded(addedToken: Pair<CryptoCurrency, ChooseTokenAnalyticsPayload.IsSearched>) {
onNewTokenAdded.trySend(addedToken)
onSearchQuery(SearchQuery.Empty)
}
fun updateCurrenciesGroup(currenciesGroup: CurrenciesGroup)
}
data class ChooseTokenResultOld(
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val account: Account,
val isSearched: Boolean,
)
data class ChooseTokenResult(
val currency: CryptoCurrencyStatus,
val account: AccountStatus,
val wallet: UserWallet,
val analyticsPayload: Set<ChooseTokenAnalyticsPayload> = emptySet(),
) {
val walletId get() = wallet.walletId
}
sealed interface ChooseTokenAnalyticsPayload {
@Suppress("BooleanPropertyNaming")
@JvmInline
value class IsSearched(val value: Boolean) : ChooseTokenAnalyticsPayload
@JvmInline
value class ScreensSources(val value: String) : ChooseTokenAnalyticsPayload
}

View file

@ -2,110 +2,12 @@ package com.tangem.feature.swap.choosetoken.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.account.Account
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.wallet.UserWallet
import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup
import com.tangem.feature.swap.presentation.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
// todo swap make universal, encapsulate, move to some common module
internal interface ChooseTokenBridge {
// todo swap new api
val onCurrencyChosen: Channel<ChooseTokenResult>
val onClose: Channel<Unit>
// todo swap legacy api, remove
val onTokenSelected: Channel<ChooseTokenResultOld>
val onNewTokenAdded: Channel<Pair<CryptoCurrency, ChooseTokenAnalyticsPayload.IsSearched>>
val searchQueryState: StateFlow<String>
val currenciesGroup: Flow<CurrenciesGroup>
fun onTokenSelected(result: ChooseTokenResultOld) {
onTokenSelected.trySend(result)
onSearchQuery("")
}
fun onNewTokenAdded(addedToken: Pair<CryptoCurrency, ChooseTokenAnalyticsPayload.IsSearched>) {
onNewTokenAdded.trySend(addedToken)
onSearchQuery("")
}
fun onSearchQuery(query: String)
fun updateCurrenciesGroup(currenciesGroup: CurrenciesGroup)
fun onCurrencyChosen(result: ChooseTokenResult) {
onCurrencyChosen.trySend(result)
}
fun onClose() {
onClose.trySend(Unit)
onSearchQuery("")
}
interface Factory {
fun create(modelScope: CoroutineScope): ChooseTokenBridge
}
}
data class ChooseTokenResultOld(
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val account: Account,
val isSearched: Boolean,
)
data class ChooseTokenResult(
val currency: CryptoCurrencyStatus,
val account: AccountStatus,
val wallet: UserWallet,
val analyticsPayload: Set<ChooseTokenAnalyticsPayload> = emptySet(),
) {
val walletId get() = wallet.walletId
}
sealed interface ChooseTokenAnalyticsPayload {
@Suppress("BooleanPropertyNaming")
@JvmInline
value class IsSearched(val value: Boolean) : ChooseTokenAnalyticsPayload
@JvmInline
value class ScreensSources(val value: String) : ChooseTokenAnalyticsPayload
}
internal interface ChooseTokenComponent : ComposableContentComponent {
data class Params(
val bridge: ChooseTokenBridge,
val settings: Settings,
val analyticsPayload: Set<ChooseTokenAnalyticsPayload> = emptySet(),
)
data class Settings(
val title: TextReference,
val isShowMarketBlock: Boolean,
) {
companion object {
val SwapFrom = Settings(
title = resourceReference(R.string.swapping_from_title),
isShowMarketBlock = false,
)
val SwapTo = Settings(
title = resourceReference(R.string.swapping_to_title),
isShowMarketBlock = true,
)
}
}
interface Factory : ComponentFactory<Params, ChooseTokenComponent>
}

View file

@ -1,12 +1,19 @@
package com.tangem.feature.swap.choosetoken.impl
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.wallet.UserWalletId
import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge.Settings
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.feature.swap.choosetoken.api.ChooseTokenResult
import com.tangem.feature.swap.choosetoken.api.ChooseTokenResultOld
import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel.Companion.DEBOUNCE_SEARCH_DELAY
import com.tangem.feature.swap.choosetoken.impl.model.PortfolioListBlockDelegate
import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup
import com.tangem.feature.swap.models.TokenListUMData
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -16,6 +23,9 @@ import kotlinx.coroutines.flow.*
internal class DefaultChooseTokenBridge @AssistedInject constructor(
@Assisted private val modelScope: CoroutineScope,
portfolioListBlockDelegateFactory: PortfolioListBlockDelegate.Factory,
@Assisted override val settings: Settings,
@Assisted override val analyticsPayload: Set<ChooseTokenAnalyticsPayload>,
) : ChooseTokenBridge {
override val onCurrencyChosen: Channel<ChooseTokenResult> = Channel()
@ -24,24 +34,54 @@ internal class DefaultChooseTokenBridge @AssistedInject constructor(
override val onNewTokenAdded: Channel<Pair<CryptoCurrency, ChooseTokenAnalyticsPayload.IsSearched>> = Channel()
override val onClose: Channel<Unit> = Channel()
private val onSearchQuery: Channel<String> = Channel()
override val searchQueryState: StateFlow<String> = onSearchQuery.receiveAsFlow()
private val onSearchQuery: Channel<SearchQuery> = Channel()
override val searchQueryState: StateFlow<SearchQuery> = onSearchQuery.receiveAsFlow()
.debounce(DEBOUNCE_SEARCH_DELAY)
.stateIn(modelScope, SharingStarted.Eagerly, initialValue = "")
.stateIn(modelScope, SharingStarted.Eagerly, initialValue = SearchQuery.Empty)
private val portfolioListBlockDelegate: PortfolioListBlockDelegate = portfolioListBlockDelegateFactory.create(
modelScope = modelScope,
searchQueryState = searchQueryState,
)
override val tokenFilter: MutableStateFlow<(AccountStatus.CryptoPortfolio, CryptoCurrencyStatus) -> Boolean>
get() = portfolioListBlockDelegate.tokenFilter
override val portfolioListBlock: Flow<Map<UserWalletId, TokenListUMData>>
get() = portfolioListBlockDelegate.portfolioList
private val _currenciesGroupFlow = MutableStateFlow<CurrenciesGroup?>(null)
override val currenciesGroup: Flow<CurrenciesGroup> = _currenciesGroupFlow.filterNotNull()
override fun onSearchQuery(query: String) {
init {
portfolioListBlockDelegate.onTokenChosen.receiveAsFlow()
.onEach { chooseResult -> onCurrencyChosen(chooseResult) }
.launchIn(modelScope)
}
override fun onSearchQuery(query: SearchQuery) {
onSearchQuery.trySend(query)
}
override fun onCurrencyChosen(result: ChooseTokenResult) {
onCurrencyChosen.trySend(result)
onSearchQuery(SearchQuery.Empty)
}
override fun onClose() {
onClose.trySend(Unit)
onSearchQuery(SearchQuery.Empty)
}
override fun updateCurrenciesGroup(currenciesGroup: CurrenciesGroup) {
_currenciesGroupFlow.update { currenciesGroup }
}
@AssistedFactory
interface Factory : ChooseTokenBridge.Factory {
override fun create(modelScope: CoroutineScope): DefaultChooseTokenBridge
override fun create(
modelScope: CoroutineScope,
settings: Settings,
analyticsPayload: Set<ChooseTokenAnalyticsPayload>,
): DefaultChooseTokenBridge
}
}

View file

@ -17,8 +17,9 @@ import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.feature.swap.choosetoken.impl.model.ClickIntents
import com.tangem.feature.swap.choosetoken.impl.model.isSearchingState
import com.tangem.feature.swap.models.TokenListUMData
import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.toPersistentList
@ -27,7 +28,8 @@ internal class ChooseTokenListItemConverter(
private val appCurrency: AppCurrency,
private val params: TokenConverterParams,
private val clickIntents: ClickIntents,
private val searchQuery: String,
private val searchQuery: SearchQuery,
private val tokenFilter: (AccountStatus.CryptoPortfolio, CryptoCurrencyStatus) -> Boolean,
) {
private val isSearchingState: Boolean get() = searchQuery.isSearchingState
@ -46,6 +48,7 @@ internal class ChooseTokenListItemConverter(
return when (params) {
is TokenConverterParams.Account -> convertAccountList(params)
is TokenConverterParams.Wallet -> convertTokenList(
account = params.mainAccount,
tokenConverter = tokenStatusConverter(params.mainAccount),
tokenListParam = params.tokenList,
)
@ -99,7 +102,7 @@ internal class ChooseTokenListItemConverter(
)
val accountItem = converter.convert(tokenList.totalFiatBalance)
val tokenConverter = tokenStatusConverter(this)
val tokensListState = convertTokenList(tokenConverter, tokenList)
val tokensListState = convertTokenList(tokenConverter, tokenList, this)
val items = tokensListState.tokensList
return TokensListPortfolioItemConverter(
tokenItemUM = accountItem,
@ -109,10 +112,12 @@ internal class ChooseTokenListItemConverter(
).convert(Unit)
}
private fun convertTokenList(tokenConverter: TokenItemStateConverter, tokenListParam: TokenList): TokenListUMData {
val tokenList = if (isSearchingState) filterByQuery(tokenListParam) else tokenListParam
return when (tokenList) {
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(),
@ -125,21 +130,21 @@ internal class ChooseTokenListItemConverter(
}
}
private fun filterByQuery(tokenList: TokenList): TokenList {
fun List<CryptoCurrencyStatus>.filterByQuery(): List<CryptoCurrencyStatus> = filter { currency ->
currency.currency.name.contains(searchQuery, ignoreCase = true) ||
currency.currency.symbol.contains(searchQuery, ignoreCase = true)
private fun filterTokenList(tokenList: TokenList, account: AccountStatus.CryptoPortfolio): TokenList {
fun List<CryptoCurrencyStatus>.filterCurrencies(): List<CryptoCurrencyStatus> = filter { currency ->
currency.filterByQuery() && tokenFilter(account, currency)
}
return when (tokenList) {
TokenList.Empty -> TokenList.Empty
is TokenList.Ungrouped -> {
val filtered = tokenList.currencies.filterByQuery()
val filtered = tokenList.currencies.filterCurrencies()
if (filtered.isEmpty()) TokenList.Empty else tokenList.copy(currencies = filtered)
}
is TokenList.GroupedByNetwork -> {
val filteredGroups = tokenList.groups
.map { group ->
val filteredCurrencies = group.currencies.filterByQuery()
val filteredCurrencies = group.currencies.filterCurrencies()
group.copy(currencies = filteredCurrencies)
}
.filter { group -> group.currencies.isNotEmpty() }
@ -147,4 +152,11 @@ internal class ChooseTokenListItemConverter(
}
}
}
private fun CryptoCurrencyStatus.filterByQuery(): Boolean {
if (!isSearchingState) return true
val isSearchFilter = currency.name.contains(searchQuery.value, ignoreCase = true) ||
currency.symbol.contains(searchQuery.value, ignoreCase = true)
return isSearchFilter
}
}

View file

@ -9,12 +9,12 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.swap.choosetoken.api.*
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.feature.swap.choosetoken.impl.converter.SearchBarToggleTransformer
import com.tangem.feature.swap.choosetoken.impl.converter.SearchBarUpdateQueryTransformer
import com.tangem.feature.swap.choosetoken.impl.ui.*
@ -29,16 +29,12 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import javax.inject.Inject
internal val String.isSearchingState: Boolean get() = this.isNotBlank()
internal val StateFlow<String>.isSearchingState: Boolean get() = this.value.isSearchingState
@Suppress("LongParameterList")
@ModelScoped
internal class ChooseTokenModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val settingContextUseCase: SettingContextUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
portfolioListBlockDelegateFactory: PortfolioListBlockDelegate.Factory,
marketBlockDelegateFactory: MarketBlockDelegate.Factory,
paramsContainer: ParamsContainer,
) : Model() {
@ -46,23 +42,19 @@ internal class ChooseTokenModel @Inject constructor(
private val params = paramsContainer.require<ChooseTokenComponent.Params>()
private val bridge: ChooseTokenBridge = params.bridge
private val searchQueryState: StateFlow<String> = bridge.searchQueryState
private val searchQueryState: StateFlow<SearchQuery> = bridge.searchQueryState
private val isSearchingState: Boolean get() = bridge.searchQueryState.isSearchingState
private val marketBlockDelegate: MarketBlockDelegate = marketBlockDelegateFactory.create(
modelScope = modelScope,
searchQueryState = searchQueryState,
screensSourcesName = params.analyticsPayload
screensSourcesName = bridge.analyticsPayload
.filterIsInstance<ChooseTokenAnalyticsPayload.ScreensSources>()
.firstOrNull()?.value.orEmpty(),
)
private val portfolioListBlockDelegate: PortfolioListBlockDelegate = portfolioListBlockDelegateFactory.create(
modelScope = modelScope,
searchQueryState = searchQueryState,
)
val bottomSheetNavigation get() = marketBlockDelegate.addToPortfolioSlot
val addToPortfolioManager get() = marketBlockDelegate.addToPortfolioManager
private val marketsStateFlow: Flow<SwapMarketState?> = if (params.settings.isShowMarketBlock) {
private val marketsStateFlow: Flow<SwapMarketState?> = if (bridge.settings.isShowMarketBlock) {
marketBlockDelegate.marketsStateFlow
} else {
flowOf(null)
@ -128,7 +120,7 @@ internal class ChooseTokenModel @Inject constructor(
val result = ChooseTokenResultOld(
account = account,
cryptoCurrencyStatus = cryptoCurrencyStatus,
isSearched = searchQueryState.value.isNotEmpty(),
isSearched = searchQueryState.isSearchingState,
)
bridge.onTokenSelected(result)
},
@ -165,7 +157,7 @@ internal class ChooseTokenModel @Inject constructor(
val fullPortfolioBlockFlow = combine(
flow = allWalletsFlow,
flow2 = portfolioListBlockDelegate.portfolioList,
flow2 = bridge.portfolioListBlock,
flow3 = selectedWalletFlow.map { wallet -> wallet.walletId }.distinctUntilChanged(),
transform = { allWallets, portfolioList, selectedWalletId ->
val tokensListData = portfolioList[selectedWalletId] ?: return@combine null
@ -188,16 +180,6 @@ internal class ChooseTokenModel @Inject constructor(
.filterNotNull()
.distinctUntilChanged()
portfolioListBlockDelegate.onTokenItemClick.receiveAsFlow()
.onEach { (account, currencyStatus) ->
onTokenItemClick(
wallet = allWalletsFlow.value[account.accountId.userWalletId] ?: return@onEach,
account = account,
currencyStatus = currencyStatus,
)
}
.launchIn(this)
combine(
flow = fullPortfolioBlockFlow,
flow2 = settingContextUseCase.invoke(),
@ -223,19 +205,6 @@ internal class ChooseTokenModel @Inject constructor(
.flowOn(dispatchers.default)
.stateIn(modelScope, SharingStarted.Eagerly, initialValue = null)
private fun onTokenItemClick(wallet: UserWallet, account: AccountStatus, currencyStatus: CryptoCurrencyStatus) {
val analyticsPayload = setOf(
ChooseTokenAnalyticsPayload.IsSearched(isSearchingState),
)
val result = ChooseTokenResult(
account = account,
currency = currencyStatus,
wallet = wallet,
analyticsPayload = analyticsPayload,
)
bridge.onCurrencyChosen(result)
}
fun onBackClicked() {
bridge.onClose()
}
@ -254,7 +223,7 @@ internal class ChooseTokenModel @Inject constructor(
)
private fun getInitState() = ChooseTokenInitialUM(
screenTitle = params.settings.title,
screenTitle = bridge.settings.title,
onCloseClick = ::onBackClicked,
searchBar = getInitialSearchBar(),
)

View file

@ -14,6 +14,8 @@ import com.tangem.domain.markets.toSerializableParam
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.feature.swap.models.AddToPortfolioRoute
import com.tangem.feature.swap.models.market.MarketsListBatchFlowManager
import com.tangem.feature.swap.models.market.state.SwapMarketState
@ -33,7 +35,7 @@ internal class MarketBlockDelegate @AssistedInject constructor(
private val getUserWalletsUseCase: GetWalletsUseCase,
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
@Assisted private val modelScope: CoroutineScope,
@Assisted private val searchQueryState: StateFlow<String>,
@Assisted private val searchQueryState: StateFlow<SearchQuery>,
@Assisted private val screensSourcesName: String,
) {
@ -49,7 +51,7 @@ internal class MarketBlockDelegate @AssistedInject constructor(
val marketsStateFlow: Flow<SwapMarketState> = searchQueryState
// Switch between default and search market flows
.map { it.isEmpty() }
.map { it.value.isEmpty() }
.distinctUntilChanged()
.flatMapLatest { isDefaultMode ->
if (isDefaultMode) {
@ -74,7 +76,7 @@ internal class MarketBlockDelegate @AssistedInject constructor(
marketsListBatchFlowManagerFactory.create(
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
order = TokenMarketListConfig.Order.ByRating,
currentSearchText = Provider { searchQueryState.value },
currentSearchText = Provider { searchQueryState.value.value },
modelScope = modelScope,
)
}
@ -83,8 +85,8 @@ internal class MarketBlockDelegate @AssistedInject constructor(
// Reload search markets when query changes
searchQueryState
.onEach { searchQuery ->
if (searchQuery.isNotEmpty()) {
searchMarketsListManager.reload(searchQuery)
if (searchQuery.isSearchingState) {
searchMarketsListManager.reload(searchQuery.value)
}
}
.launchIn(modelScope)
@ -157,7 +159,7 @@ internal class MarketBlockDelegate @AssistedInject constructor(
) { uiItems, isError, isSearchNotFound, total ->
when {
isError -> SwapMarketState.LoadingError(
onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value) },
onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value.value) },
marketsTitle = marketsTitle,
shouldAssetsCount = true,
)
@ -210,7 +212,7 @@ internal class MarketBlockDelegate @AssistedInject constructor(
@AssistedFactory
interface Factory {
fun create(
searchQueryState: StateFlow<String>,
searchQueryState: StateFlow<SearchQuery>,
modelScope: CoroutineScope,
screensSourcesName: String,
): MarketBlockDelegate

View file

@ -11,6 +11,10 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.feature.swap.choosetoken.api.ChooseTokenResult
import com.tangem.feature.swap.choosetoken.api.SettingContextUseCase
import com.tangem.feature.swap.choosetoken.impl.converter.ChooseTokenListItemConverter
import com.tangem.feature.swap.models.TokenListUMData
@ -28,17 +32,35 @@ internal class PortfolioListBlockDelegate @AssistedInject constructor(
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
private val getWalletsUseCase: GetWalletsUseCase,
@Assisted private val modelScope: CoroutineScope,
@Assisted private val searchQueryState: StateFlow<String>,
@Assisted private val searchQueryState: StateFlow<SearchQuery>,
) : ClickIntents {
val onTokenItemClick: Channel<Pair<AccountStatus, CryptoCurrencyStatus>> = Channel()
private val onTokenItemClick: Channel<Pair<AccountStatus, CryptoCurrencyStatus>> = Channel()
val portfolioList: Flow<Map<UserWalletId, TokenListUMData>> = flow {
val onTokenChosen: Channel<ChooseTokenResult> = Channel()
val tokenFilter: MutableStateFlow<(AccountStatus.CryptoPortfolio, CryptoCurrencyStatus) -> Boolean> =
MutableStateFlow { _, _ -> true }
val portfolioList: Flow<Map<UserWalletId, TokenListUMData>> = buildDataFlow()
.distinctUntilChanged()
.shareIn(modelScope, SharingStarted.Eagerly, replay = 1)
private fun buildDataFlow(): Flow<Map<UserWalletId, TokenListUMData>> = channelFlow {
val allAccountsFlow: Flow<LinkedHashMap<UserWalletId, AccountStatusList>> =
multiAccountStatusListSupplier.invokeAsMap()
val allWalletsFlow: Flow<LinkedHashMap<UserWalletId, UserWallet>> =
getWalletsUseCase.invokeAsMap()
val allWalletsFlow: StateFlow<LinkedHashMap<UserWalletId, UserWallet>> =
getWalletsUseCase.invokeAsMap().stateIn(this)
onTokenItemClick.receiveAsFlow()
.onEach { (account, currencyStatus) ->
onTokenItemClick(
wallet = allWalletsFlow.value[account.accountId.userWalletId] ?: return@onEach,
account = account,
currencyStatus = currencyStatus,
)
}
.launchIn(this)
val expandedAccountsMapFlow = allWalletsFlow
.map { allWallets -> allWallets.values.map { wallet -> wallet.walletId } }
@ -51,7 +73,8 @@ internal class PortfolioListBlockDelegate @AssistedInject constructor(
flow2 = allAccountsFlow,
flow3 = expandedAccountsMapFlow,
flow4 = searchQueryState,
transform = { settings, allAccounts, expandedAccountsMap, searchQuery ->
flow5 = tokenFilter,
transform = { settings, allAccounts, expandedAccountsMap, searchQuery, tokenFilter ->
allAccounts.mapNotNullValues { (walletId, statusList) ->
val expandedAccounts = expandedAccountsMap[walletId].orEmpty()
val converterParams = if (settings.isAccountsMode) {
@ -65,16 +88,15 @@ internal class PortfolioListBlockDelegate @AssistedInject constructor(
params = converterParams,
clickIntents = this@PortfolioListBlockDelegate,
searchQuery = searchQuery,
tokenFilter = tokenFilter,
).convert()
um
}
},
)
emitAll(finalFlow)
finalFlow.collectLatest { result -> channel.send(result) }
}
.distinctUntilChanged()
.shareIn(modelScope, SharingStarted.Eagerly, replay = 1)
private fun List<UserWalletId>.toExpandedAccountsMap(): Flow<Map<UserWalletId, Set<AccountId>>> {
if (isEmpty()) return flowOf(emptyMap())
@ -83,6 +105,19 @@ internal class PortfolioListBlockDelegate @AssistedInject constructor(
return combine(flows, { pairs -> pairs.toMap() })
}
private fun onTokenItemClick(wallet: UserWallet, account: AccountStatus, currencyStatus: CryptoCurrencyStatus) {
val analyticsPayload = setOf(
ChooseTokenAnalyticsPayload.IsSearched(searchQueryState.isSearchingState),
)
val result = ChooseTokenResult(
account = account,
currency = currencyStatus,
wallet = wallet,
analyticsPayload = analyticsPayload,
)
onTokenChosen.trySend(result)
}
override fun onTokenItemClick(account: AccountStatus, currencyStatus: CryptoCurrencyStatus) {
onTokenItemClick.trySend(account to currencyStatus)
}
@ -97,7 +132,7 @@ internal class PortfolioListBlockDelegate @AssistedInject constructor(
@AssistedFactory
interface Factory {
fun create(searchQueryState: StateFlow<String>, modelScope: CoroutineScope): PortfolioListBlockDelegate
fun create(searchQueryState: StateFlow<SearchQuery>, modelScope: CoroutineScope): PortfolioListBlockDelegate
}
}

View file

@ -121,12 +121,11 @@ private fun Content(state: ChooseTokenFullUM, modifier: Modifier = Modifier) {
assetsTitle()
if (state.contentUM != null) {
walletListItem(state.contentUM.walletList)
when {
state.contentUM.isNotFoundState -> tokensNotFound()
state.contentUM.isEmptyState -> emptyTokensList()
else -> {
walletListItem(state.contentUM.walletList)
tokensListItems(
tokensListData = state.contentUM.tokensListData,
isBalanceHidden = state.contentUM.isBalanceHidden,

View file

@ -72,6 +72,7 @@ import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNet
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.swap.analytics.SwapEvents
import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge
import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent
import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter
@ -179,7 +180,13 @@ internal class SwapModel @Inject constructor(
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
val chooseTokenBridge: ChooseTokenBridge = chooseTokenBridgeFactory.create(modelScope)
val chooseTokenBridge: ChooseTokenBridge = chooseTokenBridgeFactory.create(
modelScope = modelScope,
settings = ChooseTokenBridge.Settings.SwapTo,
analyticsPayload = setOf(
ChooseTokenAnalyticsPayload.ScreensSources(ScreensSources.Swap.value),
),
)
private val stateBuilder = StateBuilder(
userWalletProvider = Provider { userWallet },
@ -287,7 +294,7 @@ internal class SwapModel @Inject constructor(
init {
chooseTokenBridge.searchQueryState
.onEach { query -> onSearchEntered(query) }
.onEach { query -> onSearchEntered(query.value) }
.launchIn(modelScope)
chooseTokenBridge.onNewTokenAdded.receiveAsFlow()

View file

@ -15,7 +15,7 @@ internal data class SwapSelectTokenStateHolder(
)
@Immutable
internal sealed interface TokenListUMData {
sealed interface TokenListUMData {
val tokensList: ImmutableList<TokensListItemUM>
val totalTokensCount: Int