Updated on 2026-08-14
This commit is contained in:
parent
7a197ec6c9
commit
c662136303
26 changed files with 906 additions and 346 deletions
|
|
@ -37,6 +37,7 @@ dependencies {
|
|||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.staking)
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.transformers
|
||||
package com.tangem.common.ui.tokens
|
||||
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
|
||||
sealed interface TokenConverterParams {
|
||||
/** Wallet mode; list of tokens for main account */
|
||||
data class Wallet(
|
||||
val accountId: AccountId,
|
||||
val mainAccount: AccountStatus,
|
||||
val tokenList: TokenList,
|
||||
) : TokenConverterParams
|
||||
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.common.ui.tokens
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup
|
||||
|
||||
object TokenItemGrouping {
|
||||
|
||||
fun TokenList.GroupedByNetwork.toGroupedItems(tokenConverter: TokenItemStateConverter): List<TokensListItemUM> {
|
||||
return groups.fold(initial = mutableListOf()) { acc, group ->
|
||||
acc.addGroup(tokenConverter, group)
|
||||
}
|
||||
}
|
||||
|
||||
fun TokenList.Ungrouped.toUngroupedItems(tokenConverter: TokenItemStateConverter): List<TokensListItemUM> {
|
||||
return currencies.fold(initial = mutableListOf()) { acc, token ->
|
||||
acc.addToken(tokenConverter, token)
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableList<TokensListItemUM>.addGroup(
|
||||
tokenConverter: TokenItemStateConverter,
|
||||
group: NetworkGroup,
|
||||
): MutableList<TokensListItemUM> {
|
||||
val groupTitle = TokensListItemUM.GroupTitle(
|
||||
id = group.network.hashCode(),
|
||||
text = resourceReference(
|
||||
id = R.string.wallet_network_group_title,
|
||||
formatArgs = wrappedList(group.network.name),
|
||||
),
|
||||
)
|
||||
|
||||
add(groupTitle)
|
||||
group.currencies.forEach { token -> addToken(tokenConverter, token) }
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
fun MutableList<TokensListItemUM>.addToken(
|
||||
tokenConverter: TokenItemStateConverter,
|
||||
token: CryptoCurrencyStatus,
|
||||
): MutableList<TokensListItemUM> {
|
||||
val tokenItemState = tokenConverter.convert(token)
|
||||
|
||||
add(TokensListItemUM.Token(tokenItemState))
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
package com.tangem.utils.extensions
|
||||
|
||||
fun <K, V, R> Map<K, V>.mapNotNullValues(transform: (Map.Entry<K, V>) -> R?): Map<K, R> {
|
||||
return this
|
||||
.mapNotNull { entry ->
|
||||
val newValue = transform(entry) ?: return@mapNotNull null
|
||||
|
||||
entry.key to newValue
|
||||
}
|
||||
.toMap()
|
||||
inline fun <K, V, R> Map<K, V>.mapNotNullValues(transform: (Map.Entry<K, V>) -> R?): Map<K, R> {
|
||||
val result = linkedMapOf<K, R>()
|
||||
this.forEach { entry ->
|
||||
val newValue = transform(entry) ?: return@forEach
|
||||
result[entry.key] = newValue
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -3,7 +3,9 @@ package com.tangem.domain.account.status.supplier
|
|||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.producer.MultiAccountStatusListProducer
|
||||
import com.tangem.domain.core.flow.FlowCachingSupplier
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* Supplier that provides a list of [AccountStatusList]s for all user wallets.
|
||||
|
|
@ -18,4 +20,12 @@ abstract class MultiAccountStatusListSupplier(
|
|||
operator fun invoke(): Flow<List<AccountStatusList>> {
|
||||
return super.invoke(params = Unit)
|
||||
}
|
||||
|
||||
fun invokeAsMap(): Flow<LinkedHashMap<UserWalletId, AccountStatusList>> = invoke()
|
||||
.map { accountLists ->
|
||||
accountLists.associateByTo(
|
||||
destination = linkedMapOf(),
|
||||
keySelector = { accountList -> accountList.userWalletId },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.feature.wallet.presentation.account
|
||||
package com.tangem.domain.account.status.utils
|
||||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.domain.account.models.AccountExpandedState
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.repository.AccountsExpandedRepository
|
||||
|
|
@ -16,9 +15,11 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@ModelScoped
|
||||
internal class ExpandedAccountsHolder @Inject constructor(
|
||||
// todo swap separate for main and swap
|
||||
@Singleton
|
||||
class ExpandedAccountsHolder @Inject constructor(
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val accountsExpandedRepository: AccountsExpandedRepository,
|
||||
|
|
@ -30,9 +31,9 @@ internal class ExpandedAccountsHolder @Inject constructor(
|
|||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
fun expandedAccounts(userWallet: UserWallet): Flow<Set<AccountId>> = channelFlow {
|
||||
val walletId = userWallet.walletId
|
||||
fun expandedAccounts(userWallet: UserWallet): Flow<Set<AccountId>> = expandedAccounts(userWallet.walletId)
|
||||
|
||||
fun expandedAccounts(walletId: UserWalletId): Flow<Set<AccountId>> = channelFlow {
|
||||
val storedState = accountsExpandedRepository.expandedAccounts
|
||||
.map { it[walletId].orEmpty() }
|
||||
.stateIn(this)
|
||||
|
|
@ -65,7 +66,7 @@ internal class ExpandedAccountsHolder @Inject constructor(
|
|||
walletAccounts(walletId).onEach { accountList ->
|
||||
if (!isAccountsModeEnabledUseCase.invokeSync()) {
|
||||
accountsExpandedRepository.clearStore()
|
||||
expandedAccounts.update { setOf() }
|
||||
expandedAccounts.update { emptySet() }
|
||||
return@onEach
|
||||
}
|
||||
val idsSet = accountList.accounts.mapTo(mutableSetOf()) { it.accountId }
|
||||
|
|
@ -88,7 +89,7 @@ internal class ExpandedAccountsHolder @Inject constructor(
|
|||
if (isAccountMode) {
|
||||
channel.send(expanded)
|
||||
} else {
|
||||
channel.send(setOf())
|
||||
channel.send(emptySet())
|
||||
}
|
||||
},
|
||||
).collect()
|
||||
|
|
@ -2,8 +2,10 @@ package com.tangem.domain.wallets.usecase
|
|||
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.util.LinkedHashMap
|
||||
|
||||
/**
|
||||
* Use case for getting list of user wallets
|
||||
|
|
@ -19,6 +21,16 @@ class GetWalletsUseCase(
|
|||
@Throws(IllegalArgumentException::class)
|
||||
operator fun invoke(): Flow<List<UserWallet>> = userWalletsListRepository.userWallets.map { requireNotNull(it) }
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun invokeAsMap(): Flow<LinkedHashMap<UserWalletId, UserWallet>> = userWalletsListRepository.userWallets
|
||||
.map { requireNotNull(it) }
|
||||
.map { wallets ->
|
||||
wallets.associateByTo(
|
||||
destination = linkedMapOf(),
|
||||
keySelector = { wallet -> wallet.walletId },
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun invokeSync(): List<UserWallet> = userWalletsListRepository.userWallets.value!!
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ interface AddToPortfolioComponent : ComposableBottomSheetComponent {
|
|||
|
||||
interface Callback {
|
||||
fun onDismiss()
|
||||
// todo swap add new onSuccess with full data of added token
|
||||
fun onSuccess(addedToken: CryptoCurrency)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ dependencies {
|
|||
|
||||
/** Domain modules **/
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ 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
|
||||
|
|
@ -21,6 +22,7 @@ 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
|
||||
|
|
@ -52,7 +54,13 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
private val chooseTokenComponent by lazy {
|
||||
chooseTokenComponentFactory.create(
|
||||
context = child("chooseTokenComponent"),
|
||||
params = ChooseTokenComponent.Params(model.chooseTokenBridge),
|
||||
params = ChooseTokenComponent.Params(
|
||||
bridge = model.chooseTokenBridge,
|
||||
settings = ChooseTokenComponent.Settings.SwapTo,
|
||||
analyticsPayload = setOf(
|
||||
ChooseTokenAnalyticsPayload.ScreensSources(ScreensSources.Swap.value),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ package com.tangem.feature.swap.choosetoken.api
|
|||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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
|
||||
|
|
@ -56,13 +58,12 @@ internal interface ChooseTokenBridge {
|
|||
}
|
||||
|
||||
data class ChooseTokenResult(
|
||||
val addedCurrency: AccountCryptoCurrencyStatus,
|
||||
val userWallet: UserWallet,
|
||||
val currency: CryptoCurrencyStatus,
|
||||
val account: AccountStatus,
|
||||
val wallet: UserWallet,
|
||||
val analyticsPayload: Set<ChooseTokenAnalyticsPayload> = emptySet(),
|
||||
) {
|
||||
val status: CryptoCurrencyStatus get() = addedCurrency.status
|
||||
val account: Account.CryptoPortfolio get() = addedCurrency.account
|
||||
val walletId get() = userWallet.walletId
|
||||
val walletId get() = wallet.walletId
|
||||
}
|
||||
|
||||
sealed interface ChooseTokenAnalyticsPayload {
|
||||
|
|
@ -70,13 +71,34 @@ 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>
|
||||
}
|
||||
|
|
@ -38,11 +38,14 @@ internal class DefaultChooseTokenComponent @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val stateOld by model.stateOld.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
state?.let { stateHolder ->
|
||||
stateOld?.let { stateHolder ->
|
||||
SwapSelectTokenScreen(state = stateHolder, onBack = { model.onBackClicked() })
|
||||
}
|
||||
// todo swap uncomment
|
||||
// val state by model.state.collectAsStateWithLifecycle()
|
||||
// ChooseTokenScreen(state = state)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
package com.tangem.feature.swap.choosetoken.impl.converter
|
||||
|
||||
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
|
||||
import com.tangem.common.ui.account.TokensListPortfolioItemConverter
|
||||
import com.tangem.common.ui.tokens.TokenConverterParams
|
||||
import com.tangem.common.ui.tokens.TokenItemGrouping.toGroupedItems
|
||||
import com.tangem.common.ui.tokens.TokenItemGrouping.toUngroupedItems
|
||||
import com.tangem.common.ui.tokens.TokenItemStateConverter
|
||||
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.Account
|
||||
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.impl.model.ClickIntents
|
||||
import com.tangem.feature.swap.choosetoken.impl.model.isSearchingState
|
||||
import com.tangem.feature.swap.models.TokenListUMData
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class ChooseTokenListItemConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val params: TokenConverterParams,
|
||||
private val clickIntents: ClickIntents,
|
||||
private val searchQuery: String,
|
||||
) {
|
||||
|
||||
private val isSearchingState: Boolean get() = searchQuery.isSearchingState
|
||||
|
||||
private val onTokenClick: (account: AccountStatus, currencyStatus: CryptoCurrencyStatus) -> Unit =
|
||||
{ account, currencyStatus ->
|
||||
clickIntents.onTokenItemClick(account, currencyStatus)
|
||||
}
|
||||
|
||||
private fun tokenStatusConverter(account: AccountStatus) = TokenItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
onItemClick = { _, status -> onTokenClick(account, status) },
|
||||
)
|
||||
|
||||
fun convert(): TokenListUMData {
|
||||
return when (params) {
|
||||
is TokenConverterParams.Account -> convertAccountList(params)
|
||||
is TokenConverterParams.Wallet -> convertTokenList(
|
||||
tokenConverter = tokenStatusConverter(params.mainAccount),
|
||||
tokenListParam = params.tokenList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertAccountList(params: TokenConverterParams.Account): TokenListUMData {
|
||||
val accountList = params.accountList
|
||||
val accountItems = accountList.accountStatuses
|
||||
.filterCryptoPortfolio()
|
||||
.map { accountStatus -> accountStatus.toPortfolioItem(params) }
|
||||
.filter { portfolio -> portfolio.tokens.isNotEmpty() }
|
||||
if (accountItems.isEmpty()) {
|
||||
return TokenListUMData.EmptyList
|
||||
}
|
||||
val accountsList = accountItems.toPersistentList()
|
||||
return TokenListUMData.AccountList(
|
||||
tokensList = accountsList,
|
||||
totalTokensCount = accountsList.size,
|
||||
)
|
||||
}
|
||||
|
||||
private fun AccountStatus.CryptoPortfolio.toPortfolioItem(
|
||||
params: TokenConverterParams.Account,
|
||||
): TokensListItemUM.Portfolio {
|
||||
val tokenList: TokenList = this.tokenList
|
||||
val account: Account.CryptoPortfolio = this.account
|
||||
val isExpanded = isSearchingState || params.expandedAccounts.contains(account.accountId)
|
||||
val onItemClick: (Account.CryptoPortfolio) -> Unit = { clickedAccount ->
|
||||
if (isExpanded) {
|
||||
clickIntents.onAccountCollapseClick(clickedAccount)
|
||||
} else {
|
||||
clickIntents.onAccountExpandClick(clickedAccount)
|
||||
}
|
||||
}
|
||||
val converter = AccountCryptoPortfolioItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
account = account,
|
||||
onItemClick = onItemClick.takeIf { !isSearchingState },
|
||||
priceChangeLce = this.priceChangeLce,
|
||||
)
|
||||
val accountItem = converter.convert(tokenList.totalFiatBalance)
|
||||
val tokenConverter = tokenStatusConverter(this)
|
||||
val tokensListState = convertTokenList(tokenConverter, tokenList)
|
||||
val items = tokensListState.tokensList
|
||||
return TokensListPortfolioItemConverter(
|
||||
tokenItemUM = accountItem,
|
||||
isExpanded = isExpanded,
|
||||
isCollapsable = !isSearchingState,
|
||||
tokens = items.filterIsInstance<PortfolioTokensListItemUM>().toPersistentList(),
|
||||
).convert(Unit)
|
||||
}
|
||||
|
||||
private fun convertTokenList(tokenConverter: TokenItemStateConverter, tokenListParam: TokenList): TokenListUMData {
|
||||
val tokenList = if (isSearchingState) filterByQuery(tokenListParam) else tokenListParam
|
||||
|
||||
return when (tokenList) {
|
||||
is TokenList.Empty -> TokenListUMData.EmptyList
|
||||
is TokenList.GroupedByNetwork -> tokenList.toGroupedItems(tokenConverter).let { grouped ->
|
||||
TokenListUMData.TokenList(
|
||||
tokensList = grouped.toPersistentList(),
|
||||
totalTokensCount = grouped.size,
|
||||
)
|
||||
}
|
||||
is TokenList.Ungrouped -> tokenList.toUngroupedItems(tokenConverter).let { ungrouped ->
|
||||
TokenListUMData.TokenList(
|
||||
tokensList = ungrouped.toPersistentList(),
|
||||
totalTokensCount = ungrouped.size,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return when (tokenList) {
|
||||
TokenList.Empty -> TokenList.Empty
|
||||
is TokenList.Ungrouped -> {
|
||||
val filtered = tokenList.currencies.filterByQuery()
|
||||
if (filtered.isEmpty()) TokenList.Empty else tokenList.copy(currencies = filtered)
|
||||
}
|
||||
is TokenList.GroupedByNetwork -> {
|
||||
val filteredGroups = tokenList.groups
|
||||
.map { group ->
|
||||
val filteredCurrencies = group.currencies.filterByQuery()
|
||||
group.copy(currencies = filteredCurrencies)
|
||||
}
|
||||
.filter { group -> group.currencies.isNotEmpty() }
|
||||
if (filteredGroups.isEmpty()) TokenList.Empty else tokenList.copy(groups = filteredGroups)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.feature.swap.choosetoken.impl.converter
|
||||
|
||||
import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenInitialUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SearchBarToggleTransformer(private val isActive: Boolean) : Transformer<ChooseTokenInitialUM> {
|
||||
|
||||
override fun transform(prevState: ChooseTokenInitialUM): ChooseTokenInitialUM {
|
||||
return prevState.copy(
|
||||
searchBar = prevState.searchBar.copy(isActive = isActive),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.feature.swap.choosetoken.impl.converter
|
||||
|
||||
import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenInitialUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SearchBarUpdateQueryTransformer(private val newQuery: String) : Transformer<ChooseTokenInitialUM> {
|
||||
|
||||
override fun transform(prevState: ChooseTokenInitialUM): ChooseTokenInitialUM {
|
||||
return prevState.copy(
|
||||
searchBar = prevState.searchBar.copy(query = newQuery),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,112 +1,118 @@
|
|||
package com.tangem.feature.swap.choosetoken.impl.model
|
||||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketListConfig
|
||||
import com.tangem.domain.markets.toSerializableParam
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.ds.button.TangemButtonType
|
||||
import com.tangem.core.ui.ds.button.TangemButtonUM
|
||||
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.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.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload
|
||||
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge
|
||||
import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent
|
||||
import com.tangem.feature.swap.choosetoken.api.SettingContextUseCase
|
||||
import com.tangem.feature.swap.choosetoken.api.*
|
||||
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.ChooseTokenFullUM
|
||||
import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenInitialUM
|
||||
import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenUM
|
||||
import com.tangem.feature.swap.choosetoken.impl.ui.WalletListUM
|
||||
import com.tangem.feature.swap.converters.TokensDataConverter
|
||||
import com.tangem.feature.swap.models.AddToPortfolioRoute
|
||||
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
|
||||
import com.tangem.feature.swap.models.market.MarketsListBatchFlowManager
|
||||
import com.tangem.feature.swap.models.TokenListUMData
|
||||
import com.tangem.feature.swap.models.market.state.SwapMarketState
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
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 addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val getUserWalletsUseCase: GetWalletsUseCase,
|
||||
private val settingContextUseCase: SettingContextUseCase,
|
||||
private val marketsListBatchFlowManagerFactory: MarketsListBatchFlowManager.Factory,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
portfolioListBlockDelegateFactory: PortfolioListBlockDelegate.Factory,
|
||||
marketBlockDelegateFactory: MarketBlockDelegate.Factory,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ChooseTokenComponent.Params>()
|
||||
private val bridge: ChooseTokenBridge = params.bridge
|
||||
private val searchQueryState = bridge.searchQueryState
|
||||
|
||||
private val addToPortfolioJobHolder = JobHolder()
|
||||
private val searchQueryState: StateFlow<String> = bridge.searchQueryState
|
||||
private val isSearchingState: Boolean get() = bridge.searchQueryState.isSearchingState
|
||||
private val marketBlockDelegate: MarketBlockDelegate = marketBlockDelegateFactory.create(
|
||||
modelScope = modelScope,
|
||||
searchQueryState = searchQueryState,
|
||||
screensSourcesName = params.analyticsPayload
|
||||
.filterIsInstance<ChooseTokenAnalyticsPayload.ScreensSources>()
|
||||
.firstOrNull()?.value.orEmpty(),
|
||||
)
|
||||
private val portfolioListBlockDelegate: PortfolioListBlockDelegate = portfolioListBlockDelegateFactory.create(
|
||||
modelScope = modelScope,
|
||||
searchQueryState = searchQueryState,
|
||||
)
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute> = SlotNavigation()
|
||||
|
||||
private val visibleMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
|
||||
private val visibleDefaultMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
|
||||
val bottomSheetNavigation get() = marketBlockDelegate.addToPortfolioSlot
|
||||
val addToPortfolioManager get() = marketBlockDelegate.addToPortfolioManager
|
||||
private val marketsStateFlow: Flow<SwapMarketState?> = if (params.settings.isShowMarketBlock) {
|
||||
marketBlockDelegate.marketsStateFlow
|
||||
} else {
|
||||
flowOf(null)
|
||||
}
|
||||
|
||||
private val expandedAccountsFlow: MutableStateFlow<Map<AccountId, Boolean>> = MutableStateFlow(emptyMap())
|
||||
private val onWalletSelected = Channel<UserWalletId>()
|
||||
|
||||
var addToPortfolioManager: AddToPortfolioManager? = null
|
||||
// todo swap call new api result
|
||||
val addToPortfolioCallback = object : AddToPortfolioComponent.Callback {
|
||||
override fun onDismiss() = bottomSheetNavigation.dismiss()
|
||||
override fun onDismiss() = marketBlockDelegate.addToPortfolioSlot.dismiss()
|
||||
|
||||
override fun onSuccess(addedToken: CryptoCurrency) {
|
||||
modelScope.launch {
|
||||
val newToken = addedToken to ChooseTokenAnalyticsPayload
|
||||
.IsSearched(searchQueryState.value.isNotEmpty())
|
||||
bridge.onNewTokenAdded(newToken)
|
||||
bottomSheetNavigation.dismiss()
|
||||
}
|
||||
val newToken = addedToken to ChooseTokenAnalyticsPayload.IsSearched(isSearchingState)
|
||||
bridge.onNewTokenAdded(newToken)
|
||||
marketBlockDelegate.addToPortfolioSlot.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private val defaultMarketsListManager by lazy {
|
||||
marketsListBatchFlowManagerFactory.create(
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main,
|
||||
order = TokenMarketListConfig.Order.Trending,
|
||||
currentSearchText = Provider { null },
|
||||
modelScope = modelScope,
|
||||
)
|
||||
}
|
||||
val stateOld: StateFlow<SwapSelectTokenStateHolder?> = combineUIOld()
|
||||
|
||||
private val searchMarketsListManager by lazy {
|
||||
marketsListBatchFlowManagerFactory.create(
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
|
||||
order = TokenMarketListConfig.Order.ByRating,
|
||||
currentSearchText = Provider { searchQueryState.value },
|
||||
modelScope = modelScope,
|
||||
)
|
||||
}
|
||||
private val contentState: StateFlow<ChooseTokenUM?> = combineUI()
|
||||
private val initialState: MutableStateFlow<ChooseTokenInitialUM> = MutableStateFlow(getInitState())
|
||||
val state: StateFlow<ChooseTokenFullUM> = combine(
|
||||
flow = initialState,
|
||||
flow2 = contentState,
|
||||
transform = { initial, content ->
|
||||
ChooseTokenFullUM(
|
||||
initialUM = initial,
|
||||
contentUM = content,
|
||||
)
|
||||
},
|
||||
).stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = ChooseTokenFullUM(initialState.value, contentState.value),
|
||||
)
|
||||
|
||||
val state: StateFlow<SwapSelectTokenStateHolder?> = combineUI()
|
||||
|
||||
init {
|
||||
subscribeMarketTokens()
|
||||
}
|
||||
|
||||
private fun combineUI(): StateFlow<SwapSelectTokenStateHolder?> = combine(
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun combineUIOld(): StateFlow<SwapSelectTokenStateHolder?> = combine(
|
||||
flow = bridge.currenciesGroup,
|
||||
flow2 = settingContextUseCase.invoke(),
|
||||
flow3 = marketsStateFlow(),
|
||||
flow3 = marketsStateFlow,
|
||||
flow4 = expandedAccountsFlow,
|
||||
transform = { currenciesGroup, settingContext, marketState, expandedAccounts ->
|
||||
val isAccountsMode = settingContext.isAccountsMode
|
||||
|
|
@ -117,7 +123,7 @@ internal class ChooseTokenModel @Inject constructor(
|
|||
onSearchEntered = { query -> bridge.onSearchQuery(query) },
|
||||
onTokenClick = { tokenId ->
|
||||
val selected = tokenId to ChooseTokenAnalyticsPayload
|
||||
.IsSearched(searchQueryState.value.isNotEmpty())
|
||||
.IsSearched(isSearchingState)
|
||||
bridge.onTokenSelected(selected)
|
||||
},
|
||||
onAccountClick = { account ->
|
||||
|
|
@ -132,170 +138,124 @@ internal class ChooseTokenModel @Inject constructor(
|
|||
isBalanceHidden = isBalanceHidden,
|
||||
isAccountsMode = isAccountsMode,
|
||||
appCurrency = appCurrency,
|
||||
marketState = marketState,
|
||||
marketState = requireNotNull(marketState),
|
||||
).transform()
|
||||
},
|
||||
)
|
||||
.flowOn(dispatchers.default)
|
||||
.stateIn(modelScope, SharingStarted.Eagerly, initialValue = null)
|
||||
|
||||
private fun marketsStateFlow() = searchQueryState
|
||||
// Switch between default and search market flows
|
||||
.map { it.isEmpty() }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { isDefaultMode ->
|
||||
if (isDefaultMode) {
|
||||
visibleMarketItemIds.value = emptyList()
|
||||
createDefaultMarketsFlow()
|
||||
} else {
|
||||
visibleDefaultMarketItemIds.value = emptyList()
|
||||
createSearchMarketsFlow()
|
||||
}
|
||||
}
|
||||
@Suppress("LongMethod")
|
||||
private fun combineUI(): StateFlow<ChooseTokenUM?> = channelFlow {
|
||||
val allWalletsFlow: StateFlow<LinkedHashMap<UserWalletId, UserWallet>> =
|
||||
getWalletsUseCase.invokeAsMap().stateIn(this)
|
||||
|
||||
private fun subscribeMarketTokens() {
|
||||
// Reload search markets when query changes
|
||||
searchQueryState
|
||||
.onEach { searchQuery ->
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
searchMarketsListManager.reload(searchQuery)
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
val selectedWalletFlow: StateFlow<UserWallet> =
|
||||
onWalletSelected.receiveAsFlow()
|
||||
.mapNotNull { walletId -> allWalletsFlow.value[walletId] }
|
||||
.stateIn(this, SharingStarted.Eagerly, allWalletsFlow.value.values.first())
|
||||
|
||||
// Initial load of default markets
|
||||
defaultMarketsListManager.reload()
|
||||
|
||||
visibleMarketItemIds
|
||||
.mapNotNull { rawIDS ->
|
||||
if (rawIDS.isNotEmpty()) {
|
||||
searchMarketsListManager.getBatchKeysByItemIds(rawIDS)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val selectedWalletTokensData: Flow<TokenListUMData> = combine(
|
||||
flow = selectedWalletFlow.map { wallet -> wallet.walletId }.distinctUntilChanged(),
|
||||
flow2 = portfolioListBlockDelegate.portfolioList,
|
||||
transform = { selectedWalletId, allPortfoliosData -> allPortfoliosData[selectedWalletId] },
|
||||
)
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
.transformLatest<Set<Int>, Unit> { visibleBatchKeys ->
|
||||
searchMarketsListManager.loadCharts(visibleBatchKeys)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
visibleDefaultMarketItemIds
|
||||
.mapNotNull { rawIds ->
|
||||
if (rawIds.isNotEmpty()) {
|
||||
defaultMarketsListManager.getBatchKeysByItemIds(rawIds)
|
||||
val walletListUmFlow = combine(
|
||||
flow = selectedWalletFlow,
|
||||
flow2 = allWalletsFlow,
|
||||
transform = { selectedWallet, allWallets ->
|
||||
allWallets.entries
|
||||
.map { (walletId, wallet) ->
|
||||
val type = if (selectedWallet.walletId == walletId) {
|
||||
TangemButtonType.Primary
|
||||
} else {
|
||||
TangemButtonType.Secondary
|
||||
}
|
||||
TangemButtonUM(
|
||||
text = stringReference(wallet.name),
|
||||
onClick = { onWalletSelected.trySend(walletId) },
|
||||
type = type,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
|
||||
portfolioListBlockDelegate.onTokenItemClick.receiveAsFlow()
|
||||
.onEach { (account, currencyStatus) ->
|
||||
onTokenItemClick(
|
||||
wallet = allWalletsFlow.value[account.accountId.userWalletId] ?: return@onEach,
|
||||
account = account,
|
||||
currencyStatus = currencyStatus,
|
||||
)
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
combine(
|
||||
flow = selectedWalletTokensData,
|
||||
flow2 = settingContextUseCase.invoke(),
|
||||
flow3 = marketsStateFlow,
|
||||
flow4 = walletListUmFlow,
|
||||
transform = { tokensData, settings, marketsData, walletList ->
|
||||
val walletsUM = if (walletList.size != 1) {
|
||||
WalletListUM(walletList.toPersistentList())
|
||||
} else {
|
||||
null
|
||||
WalletListUM(persistentListOf())
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
.transformLatest<Set<Int>, Unit> { visibleBatchKeys ->
|
||||
defaultMarketsListManager.loadCharts(visibleBatchKeys)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
ChooseTokenUM(
|
||||
walletList = walletsUM,
|
||||
isBalanceHidden = settings.isBalanceHidden,
|
||||
isSearching = isSearchingState,
|
||||
tokensListData = tokensData,
|
||||
marketsState = marketsData,
|
||||
)
|
||||
},
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
.collect { newUM -> channel.send(newUM) }
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.stateIn(modelScope, SharingStarted.Eagerly, initialValue = null)
|
||||
|
||||
private fun createDefaultMarketsFlow(): Flow<SwapMarketState> {
|
||||
val marketsTitle = TextReference.Res(R.string.feed_trending_now)
|
||||
return combine(
|
||||
defaultMarketsListManager.uiItems,
|
||||
defaultMarketsListManager.isInInitialLoadingErrorState,
|
||||
defaultMarketsListManager.totalCount,
|
||||
) { uiItems, isError, total ->
|
||||
when {
|
||||
isError -> SwapMarketState.LoadingError(
|
||||
onRetryClicked = { defaultMarketsListManager.reload() },
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = false,
|
||||
)
|
||||
uiItems.isEmpty() -> SwapMarketState.DefaultLoading
|
||||
else -> SwapMarketState.Content(
|
||||
items = uiItems,
|
||||
loadMore = { defaultMarketsListManager.loadMore() },
|
||||
onItemClick = { item -> addToPortfolioItem(item) },
|
||||
visibleIdsChanged = { visibleDefaultMarketItemIds.value = it },
|
||||
total = total ?: uiItems.size,
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSearchMarketsFlow(): Flow<SwapMarketState> {
|
||||
val marketsTitle = TextReference.Res(R.string.markets_common_title)
|
||||
return combine(
|
||||
flow = searchMarketsListManager.uiItems,
|
||||
flow2 = searchMarketsListManager.isInInitialLoadingErrorState,
|
||||
flow3 = searchMarketsListManager.isSearchNotFoundState,
|
||||
flow4 = searchMarketsListManager.totalCount,
|
||||
) { uiItems, isError, isSearchNotFound, total ->
|
||||
when {
|
||||
isError -> SwapMarketState.LoadingError(
|
||||
onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value) },
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = true,
|
||||
)
|
||||
isSearchNotFound -> SwapMarketState.SearchNothingFound
|
||||
uiItems.isEmpty() -> SwapMarketState.SearchLoading
|
||||
else -> SwapMarketState.Content(
|
||||
items = uiItems,
|
||||
loadMore = { searchMarketsListManager.loadMore() },
|
||||
onItemClick = { item -> addToPortfolioItem(item) },
|
||||
visibleIdsChanged = { visibleMarketItemIds.value = it },
|
||||
total = total ?: uiItems.size,
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addToPortfolioItem(item: MarketsListItemUM) {
|
||||
modelScope.launch {
|
||||
val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id)
|
||||
?: searchMarketsListManager.getTokenMarketById(item.id)
|
||||
?: return@launch
|
||||
|
||||
val param = tokenMarket.toSerializableParam()
|
||||
val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot }
|
||||
|
||||
val networks = tokenMarket.networks?.filter { network ->
|
||||
BlockchainUtils.isSupportedNetworkId(
|
||||
blockchainId = network.networkId,
|
||||
coinId = tokenMarket.id.value,
|
||||
contractAddress = network.contractAddress,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
hotExcludedBlockchains = hotWalletExcludedBlockchains,
|
||||
hasOnlyHotWallets = hasOnlyHotWallets,
|
||||
)
|
||||
}?.map { network ->
|
||||
TokenMarketInfo.Network(
|
||||
networkId = network.networkId,
|
||||
isExchangeable = false,
|
||||
contractAddress = network.contractAddress,
|
||||
decimalCount = network.decimalCount,
|
||||
)
|
||||
}.orEmpty()
|
||||
|
||||
addToPortfolioManager = addToPortfolioManagerFactory
|
||||
.create(
|
||||
scope = modelScope,
|
||||
token = param,
|
||||
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value),
|
||||
).apply {
|
||||
setTokenNetworks(networks)
|
||||
}
|
||||
|
||||
addToPortfolioManager?.state
|
||||
?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd }
|
||||
?.run { bottomSheetNavigation.activate(AddToPortfolioRoute) }
|
||||
}.saveIn(addToPortfolioJobHolder)
|
||||
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()
|
||||
}
|
||||
|
||||
private fun getInitialSearchBar(): SearchBarUM = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.common_search),
|
||||
query = "",
|
||||
isActive = false,
|
||||
onQueryChange = { query ->
|
||||
initialState.update { prevState -> SearchBarUpdateQueryTransformer(query).transform(prevState) }
|
||||
bridge.onSearchQuery(query)
|
||||
},
|
||||
onActiveChange = { isActive ->
|
||||
initialState.update { prevState -> SearchBarToggleTransformer(isActive).transform(prevState) }
|
||||
},
|
||||
)
|
||||
|
||||
private fun getInitState() = ChooseTokenInitialUM(
|
||||
screenTitle = params.settings.title,
|
||||
onCloseClick = ::onBackClicked,
|
||||
searchBar = getInitialSearchBar(),
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val DEBOUNCE_SEARCH_DELAY = 500L
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
package com.tangem.feature.swap.choosetoken.impl.model
|
||||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketListConfig
|
||||
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.models.AddToPortfolioRoute
|
||||
import com.tangem.feature.swap.models.market.MarketsListBatchFlowManager
|
||||
import com.tangem.feature.swap.models.market.state.SwapMarketState
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class MarketBlockDelegate @AssistedInject constructor(
|
||||
private val marketsListBatchFlowManagerFactory: MarketsListBatchFlowManager.Factory,
|
||||
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val getUserWalletsUseCase: GetWalletsUseCase,
|
||||
@Assisted private val modelScope: CoroutineScope,
|
||||
@Assisted private val searchQueryState: StateFlow<String>,
|
||||
@Assisted private val screensSourcesName: String,
|
||||
) {
|
||||
|
||||
private val addToPortfolioJobHolder = JobHolder()
|
||||
private val visibleMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
|
||||
private val visibleDefaultMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
|
||||
|
||||
val addToPortfolioSlot: SlotNavigation<AddToPortfolioRoute> = SlotNavigation()
|
||||
var addToPortfolioManager: AddToPortfolioManager? = null
|
||||
|
||||
val marketsStateFlow: Flow<SwapMarketState> = searchQueryState
|
||||
// Switch between default and search market flows
|
||||
.map { it.isEmpty() }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { isDefaultMode ->
|
||||
if (isDefaultMode) {
|
||||
visibleMarketItemIds.value = emptyList()
|
||||
createDefaultMarketsFlow()
|
||||
} else {
|
||||
visibleDefaultMarketItemIds.value = emptyList()
|
||||
createSearchMarketsFlow()
|
||||
}
|
||||
}
|
||||
|
||||
private val defaultMarketsListManager by lazy {
|
||||
marketsListBatchFlowManagerFactory.create(
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main,
|
||||
order = TokenMarketListConfig.Order.Trending,
|
||||
currentSearchText = Provider { null },
|
||||
modelScope = modelScope,
|
||||
)
|
||||
}
|
||||
|
||||
private val searchMarketsListManager by lazy {
|
||||
marketsListBatchFlowManagerFactory.create(
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
|
||||
order = TokenMarketListConfig.Order.ByRating,
|
||||
currentSearchText = Provider { searchQueryState.value },
|
||||
modelScope = modelScope,
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
// Reload search markets when query changes
|
||||
searchQueryState
|
||||
.onEach { searchQuery ->
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
searchMarketsListManager.reload(searchQuery)
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
// Initial load of default markets
|
||||
defaultMarketsListManager.reload()
|
||||
|
||||
visibleMarketItemIds
|
||||
.mapNotNull { rawIDS ->
|
||||
if (rawIDS.isNotEmpty()) {
|
||||
searchMarketsListManager.getBatchKeysByItemIds(rawIDS)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.transformLatest<Set<Int>, Unit> { visibleBatchKeys ->
|
||||
searchMarketsListManager.loadCharts(visibleBatchKeys)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
visibleDefaultMarketItemIds
|
||||
.mapNotNull { rawIds ->
|
||||
if (rawIds.isNotEmpty()) {
|
||||
defaultMarketsListManager.getBatchKeysByItemIds(rawIds)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
.transformLatest<Set<Int>, Unit> { visibleBatchKeys ->
|
||||
defaultMarketsListManager.loadCharts(visibleBatchKeys)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun createDefaultMarketsFlow(): Flow<SwapMarketState> {
|
||||
val marketsTitle = TextReference.Res(R.string.feed_trending_now)
|
||||
return combine(
|
||||
defaultMarketsListManager.uiItems,
|
||||
defaultMarketsListManager.isInInitialLoadingErrorState,
|
||||
defaultMarketsListManager.totalCount,
|
||||
) { uiItems, isError, total ->
|
||||
when {
|
||||
isError -> SwapMarketState.LoadingError(
|
||||
onRetryClicked = { defaultMarketsListManager.reload() },
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = false,
|
||||
)
|
||||
uiItems.isEmpty() -> SwapMarketState.DefaultLoading
|
||||
else -> SwapMarketState.Content(
|
||||
items = uiItems,
|
||||
loadMore = { defaultMarketsListManager.loadMore() },
|
||||
onItemClick = { item -> addToPortfolioItem(item) },
|
||||
visibleIdsChanged = { visibleDefaultMarketItemIds.value = it },
|
||||
total = total ?: uiItems.size,
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSearchMarketsFlow(): Flow<SwapMarketState> {
|
||||
val marketsTitle = TextReference.Res(R.string.markets_common_title)
|
||||
return combine(
|
||||
flow = searchMarketsListManager.uiItems,
|
||||
flow2 = searchMarketsListManager.isInInitialLoadingErrorState,
|
||||
flow3 = searchMarketsListManager.isSearchNotFoundState,
|
||||
flow4 = searchMarketsListManager.totalCount,
|
||||
) { uiItems, isError, isSearchNotFound, total ->
|
||||
when {
|
||||
isError -> SwapMarketState.LoadingError(
|
||||
onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value) },
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = true,
|
||||
)
|
||||
isSearchNotFound -> SwapMarketState.SearchNothingFound
|
||||
uiItems.isEmpty() -> SwapMarketState.SearchLoading
|
||||
else -> SwapMarketState.Content(
|
||||
items = uiItems,
|
||||
loadMore = { searchMarketsListManager.loadMore() },
|
||||
onItemClick = { item -> addToPortfolioItem(item) },
|
||||
visibleIdsChanged = { visibleMarketItemIds.value = it },
|
||||
total = total ?: uiItems.size,
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addToPortfolioItem(item: MarketsListItemUM) {
|
||||
modelScope.launch {
|
||||
val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id)
|
||||
?: searchMarketsListManager.getTokenMarketById(item.id)
|
||||
?: return@launch
|
||||
|
||||
val param = tokenMarket.toSerializableParam()
|
||||
val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot }
|
||||
|
||||
val networks = tokenMarket.networks?.filter { network ->
|
||||
BlockchainUtils.isSupportedNetworkId(
|
||||
blockchainId = network.networkId,
|
||||
coinId = tokenMarket.id.value,
|
||||
contractAddress = network.contractAddress,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
hotExcludedBlockchains = hotWalletExcludedBlockchains,
|
||||
hasOnlyHotWallets = hasOnlyHotWallets,
|
||||
)
|
||||
}?.map { network ->
|
||||
TokenMarketInfo.Network(
|
||||
networkId = network.networkId,
|
||||
isExchangeable = false,
|
||||
contractAddress = network.contractAddress,
|
||||
decimalCount = network.decimalCount,
|
||||
)
|
||||
}.orEmpty()
|
||||
|
||||
addToPortfolioManager = addToPortfolioManagerFactory
|
||||
.create(
|
||||
scope = modelScope,
|
||||
token = param,
|
||||
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = screensSourcesName),
|
||||
).apply {
|
||||
setTokenNetworks(networks)
|
||||
}
|
||||
|
||||
addToPortfolioManager?.state
|
||||
?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd }
|
||||
?.run { addToPortfolioSlot.activate(AddToPortfolioRoute) }
|
||||
}.saveIn(addToPortfolioJobHolder)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
searchQueryState: StateFlow<String>,
|
||||
modelScope: CoroutineScope,
|
||||
screensSourcesName: String,
|
||||
): MarketBlockDelegate
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package com.tangem.feature.swap.choosetoken.impl.model
|
||||
|
||||
import com.tangem.common.ui.tokens.TokenConverterParams
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.utils.ExpandedAccountsHolder
|
||||
import com.tangem.domain.models.account.Account
|
||||
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.SettingContextUseCase
|
||||
import com.tangem.feature.swap.choosetoken.impl.converter.ChooseTokenListItemConverter
|
||||
import com.tangem.feature.swap.models.TokenListUMData
|
||||
import com.tangem.utils.extensions.mapNotNullValues
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
internal class PortfolioListBlockDelegate @AssistedInject constructor(
|
||||
private val expandedAccountsHolder: ExpandedAccountsHolder,
|
||||
private val settingContext: SettingContextUseCase,
|
||||
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
@Assisted private val modelScope: CoroutineScope,
|
||||
@Assisted private val searchQueryState: StateFlow<String>,
|
||||
) : ClickIntents {
|
||||
|
||||
val onTokenItemClick: Channel<Pair<AccountStatus, CryptoCurrencyStatus>> = Channel()
|
||||
|
||||
val portfolioList: Flow<Map<UserWalletId, TokenListUMData>> = flow {
|
||||
val allAccountsFlow: Flow<LinkedHashMap<UserWalletId, AccountStatusList>> =
|
||||
multiAccountStatusListSupplier.invokeAsMap()
|
||||
|
||||
val allWalletsFlow: Flow<LinkedHashMap<UserWalletId, UserWallet>> =
|
||||
getWalletsUseCase.invokeAsMap()
|
||||
|
||||
val expandedAccountsMapFlow = allWalletsFlow
|
||||
.map { allWallets -> allWallets.values.map { wallet -> wallet.walletId } }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { walletIds -> walletIds.toExpandedAccountsMap() }
|
||||
.distinctUntilChanged()
|
||||
|
||||
val finalFlow = combine(
|
||||
flow = settingContext.invoke(),
|
||||
flow2 = allAccountsFlow,
|
||||
flow3 = expandedAccountsMapFlow,
|
||||
flow4 = searchQueryState,
|
||||
transform = { settings, allAccounts, expandedAccountsMap, searchQuery ->
|
||||
allAccounts.mapNotNullValues { (walletId, statusList) ->
|
||||
val expandedAccounts = expandedAccountsMap[walletId].orEmpty()
|
||||
val converterParams = if (settings.isAccountsMode) {
|
||||
TokenConverterParams.Account(statusList, expandedAccounts)
|
||||
} else {
|
||||
TokenConverterParams.Wallet(statusList.mainAccount, statusList.mainAccount.tokenList)
|
||||
}
|
||||
|
||||
val um = ChooseTokenListItemConverter(
|
||||
appCurrency = settings.appCurrency,
|
||||
params = converterParams,
|
||||
clickIntents = this@PortfolioListBlockDelegate,
|
||||
searchQuery = searchQuery,
|
||||
).convert()
|
||||
|
||||
um
|
||||
}
|
||||
},
|
||||
)
|
||||
emitAll(finalFlow)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.shareIn(modelScope, SharingStarted.Eagerly, replay = 1)
|
||||
|
||||
private fun List<UserWalletId>.toExpandedAccountsMap(): Flow<Map<UserWalletId, Set<AccountId>>> {
|
||||
if (isEmpty()) return flowOf(emptyMap())
|
||||
val flows: List<Flow<Pair<UserWalletId, Set<AccountId>>>> =
|
||||
map { walletId -> expandedAccountsHolder.expandedAccounts(walletId).map { set -> walletId to set } }
|
||||
return combine(flows, { pairs -> pairs.toMap() })
|
||||
}
|
||||
|
||||
override fun onTokenItemClick(account: AccountStatus, currencyStatus: CryptoCurrencyStatus) {
|
||||
onTokenItemClick.trySend(account to currencyStatus)
|
||||
}
|
||||
|
||||
override fun onAccountExpandClick(account: Account) {
|
||||
expandedAccountsHolder.expandAccount(account.accountId)
|
||||
}
|
||||
|
||||
override fun onAccountCollapseClick(account: Account) {
|
||||
expandedAccountsHolder.collapseAccount(account.accountId)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(searchQueryState: StateFlow<String>, modelScope: CoroutineScope): PortfolioListBlockDelegate
|
||||
}
|
||||
}
|
||||
|
||||
internal interface ClickIntents {
|
||||
fun onTokenItemClick(account: AccountStatus, currencyStatus: CryptoCurrencyStatus)
|
||||
|
||||
fun onAccountExpandClick(account: Account)
|
||||
|
||||
fun onAccountCollapseClick(account: Account)
|
||||
}
|
||||
|
|
@ -55,21 +55,15 @@ import kotlin.random.Random
|
|||
private const val LOAD_MORE_BUFFER = 25
|
||||
|
||||
@Composable
|
||||
internal fun ChooseTokenScreen(state: ChooseTokenUM, modifier: Modifier = Modifier) {
|
||||
internal fun ChooseTokenScreen(state: ChooseTokenFullUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.background(color = TangemTheme.colors.background.secondary)
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.imePadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
AppBar(title = state.screenTitle, onBackClick = state.onCloseClick)
|
||||
SearchBar(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
state = state.searchBar,
|
||||
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
|
||||
)
|
||||
AppBar(title = state.initialUM.screenTitle, onBackClick = state.initialUM.onCloseClick, Modifier)
|
||||
|
||||
Content(
|
||||
state = state,
|
||||
|
|
@ -79,17 +73,19 @@ internal fun ChooseTokenScreen(state: ChooseTokenUM, modifier: Modifier = Modifi
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun AppBar(title: TextReference, onBackClick: () -> Unit) {
|
||||
private fun AppBar(title: TextReference, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
AppBarWithBackButton(
|
||||
text = title.resolveReference(),
|
||||
onBackClick = onBackClick,
|
||||
iconRes = com.tangem.common.ui.R.drawable.ic_back_24,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
modifier = modifier
|
||||
.statusBarsPadding()
|
||||
.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: ChooseTokenUM, modifier: Modifier = Modifier) {
|
||||
private fun Content(state: ChooseTokenFullUM, modifier: Modifier = Modifier) {
|
||||
val nestedScrollConnection = rememberHideKeyboardNestedScrollConnection()
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
|
|
@ -99,23 +95,34 @@ private fun Content(state: ChooseTokenUM, modifier: Modifier = Modifier) {
|
|||
.nestedScroll(nestedScrollConnection),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
state = lazyListState,
|
||||
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
|
||||
) {
|
||||
item(key = "search_bar") {
|
||||
SearchBar(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
state = state.initialUM.searchBar,
|
||||
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
|
||||
)
|
||||
}
|
||||
|
||||
assetsTitle()
|
||||
|
||||
walletListItem(state.walletList)
|
||||
if (state.contentUM != null) {
|
||||
walletListItem(state.contentUM.walletList)
|
||||
|
||||
tokensListItems(
|
||||
tokensListData = state.tokensListData,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
)
|
||||
tokensListItems(
|
||||
tokensListData = state.contentUM.tokensListData,
|
||||
isBalanceHidden = state.contentUM.isBalanceHidden,
|
||||
)
|
||||
|
||||
if (state.marketsState != null) {
|
||||
item("markets_title_spacer") { SpacerH(height = 20.dp) }
|
||||
swapMarketsListItems(state.marketsState)
|
||||
if (state.contentUM.marketsState != null) {
|
||||
item("markets_title_spacer") { SpacerH(height = 20.dp) }
|
||||
swapMarketsListItems(state.contentUM.marketsState)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.marketsState != null) {
|
||||
SetupMarketScrollTracker(state.marketsState, lazyListState)
|
||||
if (state.contentUM?.marketsState != null) {
|
||||
SetupMarketScrollTracker(state.contentUM.marketsState, lazyListState)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -248,7 +255,7 @@ private fun LazyListScope.tokensList(items: ImmutableList<TokensListItemUM>, isB
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TokenScreenPreview(@PreviewParameter(ChooseTokenScreenPreviewProvider::class) state: ChooseTokenUM) {
|
||||
private fun TokenScreenPreview(@PreviewParameter(ChooseTokenScreenPreviewProvider::class) state: ChooseTokenFullUM) {
|
||||
TangemThemePreview {
|
||||
ChooseTokenScreen(
|
||||
state = state,
|
||||
|
|
@ -331,20 +338,24 @@ private val wallets
|
|||
),
|
||||
)
|
||||
|
||||
private class ChooseTokenScreenPreviewProvider : PreviewParameterProvider<ChooseTokenUM> {
|
||||
override val values: Sequence<ChooseTokenUM> = sequenceOf(
|
||||
ChooseTokenUM(
|
||||
screenTitle = stringReference("Choose token"),
|
||||
onCloseClick = {},
|
||||
walletList = WalletListUM(wallets),
|
||||
searchBar = searchBar,
|
||||
isBalanceHidden = false,
|
||||
isAfterSearch = false,
|
||||
tokensListData = TokenListUMData.AccountList(
|
||||
tokensList = accounts,
|
||||
accounts.size,
|
||||
private class ChooseTokenScreenPreviewProvider : PreviewParameterProvider<ChooseTokenFullUM> {
|
||||
override val values: Sequence<ChooseTokenFullUM> = sequenceOf(
|
||||
ChooseTokenFullUM(
|
||||
initialUM = ChooseTokenInitialUM(
|
||||
screenTitle = stringReference("Choose token"),
|
||||
onCloseClick = {},
|
||||
searchBar = searchBar,
|
||||
),
|
||||
contentUM = ChooseTokenUM(
|
||||
walletList = WalletListUM(wallets),
|
||||
isBalanceHidden = false,
|
||||
isSearching = false,
|
||||
tokensListData = TokenListUMData.AccountList(
|
||||
tokensList = accounts,
|
||||
accounts.size,
|
||||
),
|
||||
marketsState = SwapSelectTokenPreviewProvider.defaultState.marketsState,
|
||||
),
|
||||
marketsState = SwapSelectTokenPreviewProvider.defaultState.marketsState,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -7,17 +7,25 @@ import com.tangem.feature.swap.models.TokenListUMData
|
|||
import com.tangem.feature.swap.models.market.state.SwapMarketState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class ChooseTokenFullUM(
|
||||
val initialUM: ChooseTokenInitialUM,
|
||||
val contentUM: ChooseTokenUM?,
|
||||
)
|
||||
|
||||
internal data class ChooseTokenUM(
|
||||
val screenTitle: TextReference,
|
||||
val onCloseClick: () -> Unit,
|
||||
val walletList: WalletListUM,
|
||||
val searchBar: SearchBarUM,
|
||||
val isBalanceHidden: Boolean,
|
||||
val isAfterSearch: Boolean,
|
||||
val isSearching: Boolean,
|
||||
val tokensListData: TokenListUMData,
|
||||
val marketsState: SwapMarketState?,
|
||||
)
|
||||
|
||||
internal data class ChooseTokenInitialUM(
|
||||
val screenTitle: TextReference,
|
||||
val onCloseClick: () -> Unit,
|
||||
val searchBar: SearchBarUM,
|
||||
)
|
||||
|
||||
internal data class WalletListUM(
|
||||
val items: ImmutableList<TangemButtonUM>,
|
||||
)
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.account
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier
|
||||
import com.tangem.domain.account.status.utils.ExpandedAccountsHolder
|
||||
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.transformers
|
||||
|
||||
import com.tangem.common.ui.tokens.TokenConverterParams
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM
|
|||
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
|
|
@ -25,11 +24,10 @@ import com.tangem.domain.staking.model.StakingAvailability
|
|||
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams
|
||||
import com.tangem.common.ui.tokens.TokenConverterParams
|
||||
import com.tangem.common.ui.tokens.TokenItemGrouping.toGroupedItems
|
||||
import com.tangem.common.ui.tokens.TokenItemGrouping.toUngroupedItems
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import java.math.BigDecimal
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.OrganizeTokensButtonConfig as WalletOrganizeTokensButtonConfig
|
||||
|
|
@ -92,7 +90,7 @@ internal class TokenListStateConverter(
|
|||
return when (params) {
|
||||
is TokenConverterParams.Account -> convertAccountList(params)
|
||||
is TokenConverterParams.Wallet -> convertTokenList(
|
||||
tokenConverter = tokenStatusConverter(params.accountId),
|
||||
tokenConverter = tokenStatusConverter(params.mainAccount.accountId),
|
||||
tokenList = params.tokenList,
|
||||
)
|
||||
}
|
||||
|
|
@ -102,11 +100,11 @@ internal class TokenListStateConverter(
|
|||
when (tokenList) {
|
||||
is TokenList.Empty -> WalletTokensListState.Empty
|
||||
is TokenList.GroupedByNetwork -> WalletTokensListState.ContentState.Content(
|
||||
items = tokenList.toGroupedItems(tokenConverter),
|
||||
items = tokenList.toGroupedItems(tokenConverter).toPersistentList(),
|
||||
organizeTokensButtonConfig = getOrganizeTokensButtonState(tokenList = tokenList),
|
||||
)
|
||||
is TokenList.Ungrouped -> WalletTokensListState.ContentState.Content(
|
||||
items = tokenList.toUngroupedItems(tokenConverter),
|
||||
items = tokenList.toUngroupedItems(tokenConverter).toPersistentList(),
|
||||
organizeTokensButtonConfig = getOrganizeTokensButtonState(tokenList = tokenList),
|
||||
)
|
||||
}
|
||||
|
|
@ -167,51 +165,6 @@ internal class TokenListStateConverter(
|
|||
)
|
||||
}
|
||||
|
||||
private fun TokenList.GroupedByNetwork.toGroupedItems(
|
||||
tokenConverter: TokenItemStateConverter,
|
||||
): PersistentList<TokensListItemUM> {
|
||||
return groups.fold(initial = persistentListOf()) { acc, group ->
|
||||
acc.mutate { it.addGroup(tokenConverter, group) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenList.Ungrouped.toUngroupedItems(
|
||||
tokenConverter: TokenItemStateConverter,
|
||||
): PersistentList<TokensListItemUM> {
|
||||
return currencies.fold(initial = persistentListOf()) { acc, token ->
|
||||
acc.mutate { it.addToken(tokenConverter, token) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<TokensListItemUM>.addGroup(
|
||||
tokenConverter: TokenItemStateConverter,
|
||||
group: NetworkGroup,
|
||||
): List<TokensListItemUM> {
|
||||
val groupTitle = TokensListItemUM.GroupTitle(
|
||||
id = group.network.hashCode(),
|
||||
text = resourceReference(
|
||||
id = R.string.wallet_network_group_title,
|
||||
formatArgs = wrappedList(group.network.name),
|
||||
),
|
||||
)
|
||||
|
||||
add(groupTitle)
|
||||
group.currencies.forEach { token -> addToken(tokenConverter, token) }
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun MutableList<TokensListItemUM>.addToken(
|
||||
tokenConverter: TokenItemStateConverter,
|
||||
token: CryptoCurrencyStatus,
|
||||
): List<TokensListItemUM> {
|
||||
val tokenItemState = tokenConverter.convert(token)
|
||||
|
||||
add(TokensListItemUM.Token(tokenItemState))
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getOrganizeTokensButtonState(tokenList: TokenList): WalletOrganizeTokensButtonConfig? {
|
||||
val currenciesSize = when (tokenList) {
|
||||
TokenList.Empty -> return null
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.domain.account.models.AccountStatusList
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.currency.yieldSupplyKey
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams
|
||||
import com.tangem.common.ui.tokens.TokenConverterParams
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ import com.tangem.feature.wallet.presentation.account.AccountDependencies
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListErrorTransformer
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListTransformer
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams
|
||||
import com.tangem.common.ui.tokens.TokenConverterParams
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
|
@ -65,7 +66,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
|
|||
singleAccountTransform(
|
||||
maybeTokenList = maybeTokenList,
|
||||
appCurrency = appCurrency,
|
||||
accountId = mainAccount.accountId,
|
||||
mainAccount = mainAccount,
|
||||
yieldSupplyApyMap = yieldSupplyApyMap,
|
||||
stakingAvailabilityMap = stakingAvailabilityMap,
|
||||
shouldShowMainPromo = shouldShowMainPromo,
|
||||
|
|
@ -111,7 +112,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
|
|||
private fun singleAccountTransform(
|
||||
maybeTokenList: Lce<TokenListError, TokenList>,
|
||||
appCurrency: AppCurrency,
|
||||
accountId: AccountId,
|
||||
mainAccount: AccountStatus.CryptoPortfolio,
|
||||
yieldSupplyApyMap: Map<String, BigDecimal> = emptyMap(),
|
||||
stakingAvailabilityMap: Map<CryptoCurrency, StakingAvailability> = emptyMap(),
|
||||
shouldShowMainPromo: Boolean,
|
||||
|
|
@ -141,7 +142,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
|
|||
)
|
||||
|
||||
updateContent(
|
||||
params = TokenConverterParams.Wallet(accountId, tokenList),
|
||||
params = TokenConverterParams.Wallet(mainAccount, tokenList),
|
||||
appCurrency = appCurrency,
|
||||
yieldSupplyApyMap = yieldSupplyApyMap,
|
||||
stakingAvailabilityMap = stakingAvailabilityMap,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.PortfolioId
|
||||
import com.tangem.common.ui.tokens.TokenConverterParams
|
||||
import com.tangem.domain.core.utils.lceError
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.Account.CryptoPortfolio.Companion.createMainAccount
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -11,19 +13,25 @@ import com.tangem.domain.models.network.NetworkAddress
|
|||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
class YieldSupplyPromoBannerConverterTest {
|
||||
|
||||
private val account
|
||||
get() = AccountStatus.CryptoPortfolio(
|
||||
tokenList = TokenList.Empty,
|
||||
priceChangeLce = Unit.lceError(),
|
||||
account = createMainAccount(UserWalletId("00"))
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN promo disabled WHEN convert THEN return null`() {
|
||||
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xABCDEF")
|
||||
val status = createLoadedStatus(token = token, amount = BigDecimal.ONE, isYieldActive = false)
|
||||
val tokenList = ungroupedTokenList(status)
|
||||
val params = TokenConverterParams.Wallet(
|
||||
accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")),
|
||||
mainAccount = account,
|
||||
tokenList = tokenList,
|
||||
)
|
||||
val converter = YieldSupplyPromoBannerConverter(
|
||||
|
|
@ -41,7 +49,7 @@ class YieldSupplyPromoBannerConverterTest {
|
|||
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xA1")
|
||||
val status = createLoadedStatus(token = token, amount = BigDecimal("2.0"), isYieldActive = false)
|
||||
val params = TokenConverterParams.Wallet(
|
||||
accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")),
|
||||
mainAccount = account,
|
||||
tokenList = ungroupedTokenList(status),
|
||||
)
|
||||
val converter = YieldSupplyPromoBannerConverter(
|
||||
|
|
@ -59,7 +67,7 @@ class YieldSupplyPromoBannerConverterTest {
|
|||
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xAA")
|
||||
val statusActive = createLoadedStatus(token = token, amount = BigDecimal("5"), isYieldActive = true)
|
||||
val params = TokenConverterParams.Wallet(
|
||||
accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")),
|
||||
mainAccount = account,
|
||||
tokenList = ungroupedTokenList(statusActive),
|
||||
)
|
||||
val converter = YieldSupplyPromoBannerConverter(
|
||||
|
|
@ -87,7 +95,7 @@ class YieldSupplyPromoBannerConverterTest {
|
|||
)
|
||||
|
||||
val params = TokenConverterParams.Wallet(
|
||||
accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")),
|
||||
mainAccount = account,
|
||||
tokenList = ungroupedTokenList(statusSmall, statusBig),
|
||||
)
|
||||
val converter = YieldSupplyPromoBannerConverter(
|
||||
|
|
@ -110,7 +118,7 @@ class YieldSupplyPromoBannerConverterTest {
|
|||
val apyMap = mapOf(mismatchedKey to BigDecimal("0.07"))
|
||||
|
||||
val params = TokenConverterParams.Wallet(
|
||||
accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")),
|
||||
mainAccount = account,
|
||||
tokenList = ungroupedTokenList(status),
|
||||
)
|
||||
val converter = YieldSupplyPromoBannerConverter(
|
||||
|
|
@ -128,7 +136,7 @@ class YieldSupplyPromoBannerConverterTest {
|
|||
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xCUSTOM")
|
||||
val status = createCustomStatus(token = token, amount = BigDecimal("5.0"), isYieldActive = false)
|
||||
val params = TokenConverterParams.Wallet(
|
||||
accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")),
|
||||
mainAccount = account,
|
||||
tokenList = ungroupedTokenList(status),
|
||||
)
|
||||
val converter = YieldSupplyPromoBannerConverter(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue