From 70aa023245182f98cf7fe7bad88eabead6181e13 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 16 Apr 2026 13:46:34 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../common/ui/tokens/TokenConverterParams.kt | 2 +- .../feature/swap/DefaultSwapComponent.kt | 6 - .../swap/choosetoken/api/ChooseTokenBridge.kt | 126 ++++++++++++++++++ .../choosetoken/api/ChooseTokenComponent.kt | 98 -------------- .../impl/DefaultChooseTokenBridge.kt | 50 ++++++- .../converter/ChooseTokenListItemConverter.kt | 38 ++++-- .../impl/model/ChooseTokenModel.kt | 47 ++----- .../impl/model/MarketBlockDelegate.kt | 16 ++- .../impl/model/PortfolioListBlockDelegate.kt | 55 ++++++-- .../choosetoken/impl/ui/ChooseTokenScreen.kt | 3 +- .../tangem/feature/swap/model/SwapModel.kt | 11 +- .../swap/models/SwapSelectTokenStateHolder.kt | 2 +- 12 files changed, 270 insertions(+), 184 deletions(-) create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenBridge.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenConverterParams.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenConverterParams.kt index e6541f20cb..be987dde2a 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenConverterParams.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenConverterParams.kt @@ -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 diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index 27f301ffb3..09d6310350 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -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), - ), ), ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenBridge.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenBridge.kt new file mode 100644 index 0000000000..9035ff84e8 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenBridge.kt @@ -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 + val onClose: Channel + + /** + * 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 = emptySet(), + ): ChooseTokenBridge + } +} + +/** + * primary for internal impl usage, but you can also use it externally + */ +interface ChooseTokenBridgeInternal { + val settings: ChooseTokenBridge.Settings + val analyticsPayload: Set + val searchQueryState: StateFlow + val portfolioListBlock: Flow> + + 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.isSearchingState: Boolean get() = this.value.isSearchingState + } + } +} + +// todo swap legacy api, remove +interface ChooseTokenBridgeLegacy : ChooseTokenBridgeInternal { + + val onTokenSelected: Channel + val onNewTokenAdded: Channel> + + val currenciesGroup: Flow + + fun onTokenSelected(result: ChooseTokenResultOld) { + onTokenSelected.trySend(result) + onSearchQuery(SearchQuery.Empty) + } + + fun onNewTokenAdded(addedToken: Pair) { + 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 = 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 +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt index c421ec5874..36bd0585db 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt @@ -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 - val onClose: Channel - - // todo swap legacy api, remove - val onTokenSelected: Channel - val onNewTokenAdded: Channel> - - val searchQueryState: StateFlow - val currenciesGroup: Flow - - fun onTokenSelected(result: ChooseTokenResultOld) { - onTokenSelected.trySend(result) - onSearchQuery("") - } - - fun onNewTokenAdded(addedToken: Pair) { - 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 = 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 = 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 } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt index a8b7cb6142..3b3b7e6611 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt @@ -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, ) : ChooseTokenBridge { override val onCurrencyChosen: Channel = Channel() @@ -24,24 +34,54 @@ internal class DefaultChooseTokenBridge @AssistedInject constructor( override val onNewTokenAdded: Channel> = Channel() override val onClose: Channel = Channel() - private val onSearchQuery: Channel = Channel() - override val searchQueryState: StateFlow = onSearchQuery.receiveAsFlow() + private val onSearchQuery: Channel = Channel() + override val searchQueryState: StateFlow = 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> + get() = portfolioListBlockDelegate.portfolioList private val _currenciesGroupFlow = MutableStateFlow(null) override val currenciesGroup: Flow = _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, + ): DefaultChooseTokenBridge } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt index 02051e6d74..e8be72f01f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt @@ -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.filterByQuery(): List = 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.filterCurrencies(): List = 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 + } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt index d0fd563a00..22ee9d1cbf 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt @@ -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.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() private val bridge: ChooseTokenBridge = params.bridge - private val searchQueryState: StateFlow = bridge.searchQueryState + private val searchQueryState: StateFlow = 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() .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 = if (params.settings.isShowMarketBlock) { + private val marketsStateFlow: Flow = 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(), ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt index 36f5f5ebc2..e9c9ff93d4 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt @@ -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, + @Assisted private val searchQueryState: StateFlow, @Assisted private val screensSourcesName: String, ) { @@ -49,7 +51,7 @@ internal class MarketBlockDelegate @AssistedInject constructor( val marketsStateFlow: Flow = 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, + searchQueryState: StateFlow, modelScope: CoroutineScope, screensSourcesName: String, ): MarketBlockDelegate diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/PortfolioListBlockDelegate.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/PortfolioListBlockDelegate.kt index 9e45ac853a..948e0763fe 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/PortfolioListBlockDelegate.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/PortfolioListBlockDelegate.kt @@ -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, + @Assisted private val searchQueryState: StateFlow, ) : ClickIntents { - val onTokenItemClick: Channel> = Channel() + private val onTokenItemClick: Channel> = Channel() - val portfolioList: Flow> = flow { + val onTokenChosen: Channel = Channel() + val tokenFilter: MutableStateFlow<(AccountStatus.CryptoPortfolio, CryptoCurrencyStatus) -> Boolean> = + MutableStateFlow { _, _ -> true } + + val portfolioList: Flow> = buildDataFlow() + .distinctUntilChanged() + .shareIn(modelScope, SharingStarted.Eagerly, replay = 1) + + private fun buildDataFlow(): Flow> = channelFlow { val allAccountsFlow: Flow> = multiAccountStatusListSupplier.invokeAsMap() - val allWalletsFlow: Flow> = - getWalletsUseCase.invokeAsMap() + val allWalletsFlow: StateFlow> = + 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.toExpandedAccountsMap(): Flow>> { 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, modelScope: CoroutineScope): PortfolioListBlockDelegate + fun create(searchQueryState: StateFlow, modelScope: CoroutineScope): PortfolioListBlockDelegate } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt index a02d3a52c2..1081546f78 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt @@ -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, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 288c865b8e..eb2222f685 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -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 = 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() diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index 950ef764ad..1c40cf6de6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -15,7 +15,7 @@ internal data class SwapSelectTokenStateHolder( ) @Immutable -internal sealed interface TokenListUMData { +sealed interface TokenListUMData { val tokensList: ImmutableList val totalTokensCount: Int