Updated on 2026-08-14
This commit is contained in:
parent
6badbf18b5
commit
70aa023245
12 changed files with 270 additions and 184 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ internal data class SwapSelectTokenStateHolder(
|
|||
)
|
||||
|
||||
@Immutable
|
||||
internal sealed interface TokenListUMData {
|
||||
sealed interface TokenListUMData {
|
||||
|
||||
val tokensList: ImmutableList<TokensListItemUM>
|
||||
val totalTokensCount: Int
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue