diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index 6e2c8833bb..71620a30d6 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -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) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TokenConverterParams.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenConverterParams.kt similarity index 80% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TokenConverterParams.kt rename to common/ui/src/main/java/com/tangem/common/ui/tokens/TokenConverterParams.kt index 03ffab9a92..e6541f20cb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TokenConverterParams.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenConverterParams.kt @@ -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 diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemGrouping.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemGrouping.kt new file mode 100644 index 0000000000..f2b67b9645 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemGrouping.kt @@ -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 { + return groups.fold(initial = mutableListOf()) { acc, group -> + acc.addGroup(tokenConverter, group) + } + } + + fun TokenList.Ungrouped.toUngroupedItems(tokenConverter: TokenItemStateConverter): List { + return currencies.fold(initial = mutableListOf()) { acc, token -> + acc.addToken(tokenConverter, token) + } + } + + fun MutableList.addGroup( + tokenConverter: TokenItemStateConverter, + group: NetworkGroup, + ): MutableList { + 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.addToken( + tokenConverter: TokenItemStateConverter, + token: CryptoCurrencyStatus, + ): MutableList { + val tokenItemState = tokenConverter.convert(token) + + add(TokensListItemUM.Token(tokenItemState)) + + return this + } +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Map.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Map.kt index 99cf375010..dd40d06523 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/Map.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/Map.kt @@ -1,11 +1,10 @@ package com.tangem.utils.extensions -fun Map.mapNotNullValues(transform: (Map.Entry) -> R?): Map { - return this - .mapNotNull { entry -> - val newValue = transform(entry) ?: return@mapNotNull null - - entry.key to newValue - } - .toMap() +inline fun Map.mapNotNullValues(transform: (Map.Entry) -> R?): Map { + val result = linkedMapOf() + this.forEach { entry -> + val newValue = transform(entry) ?: return@forEach + result[entry.key] = newValue + } + return result } \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/MultiAccountStatusListSupplier.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/MultiAccountStatusListSupplier.kt index b37d457efc..559ab0894e 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/MultiAccountStatusListSupplier.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/MultiAccountStatusListSupplier.kt @@ -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> { return super.invoke(params = Unit) } + + fun invokeAsMap(): Flow> = invoke() + .map { accountLists -> + accountLists.associateByTo( + destination = linkedMapOf(), + keySelector = { accountList -> accountList.userWalletId }, + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/ExpandedAccountsHolder.kt similarity index 91% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt rename to domain/account/status/src/main/java/com/tangem/domain/account/status/utils/ExpandedAccountsHolder.kt index b1c48cff7e..4a33fdf3b8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/ExpandedAccountsHolder.kt @@ -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> = channelFlow { - val walletId = userWallet.walletId + fun expandedAccounts(userWallet: UserWallet): Flow> = expandedAccounts(userWallet.walletId) + fun expandedAccounts(walletId: UserWalletId): Flow> = 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() diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt index de1b19acf0..1a436c9f93 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt @@ -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> = userWalletsListRepository.userWallets.map { requireNotNull(it) } + @Throws(IllegalArgumentException::class) + fun invokeAsMap(): Flow> = userWalletsListRepository.userWallets + .map { requireNotNull(it) } + .map { wallets -> + wallets.associateByTo( + destination = linkedMapOf(), + keySelector = { wallet -> wallet.walletId }, + ) + } + @Throws(IllegalArgumentException::class) fun invokeSync(): List = userWalletsListRepository.userWallets.value!! } \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioComponent.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioComponent.kt index 902f38339d..5a1dabaa43 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioComponent.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioComponent.kt @@ -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) } diff --git a/features/swap/impl/build.gradle.kts b/features/swap/impl/build.gradle.kts index 297b57d054..6cd1167b8e 100644 --- a/features/swap/impl/build.gradle.kts +++ b/features/swap/impl/build.gradle.kts @@ -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) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index e35364d0e5..27f301ffb3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -12,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), + ), + ), ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt index 9caef19998..e87799d55e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt @@ -2,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 = 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 = emptySet(), ) + data class Settings( + val title: TextReference, + val isShowMarketBlock: Boolean, + ) { + companion object { + val SwapFrom = Settings( + title = resourceReference(R.string.swapping_from_title), + isShowMarketBlock = false, + ) + val SwapTo = Settings( + title = resourceReference(R.string.swapping_to_title), + isShowMarketBlock = true, + ) + } + } + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt index 3ac09c489f..62bd2f26e5 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt @@ -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() } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt new file mode 100644 index 0000000000..a130a0eabf --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt @@ -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().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.filterByQuery(): List = 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) + } + } + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/SearchBarToggleTransformer.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/SearchBarToggleTransformer.kt new file mode 100644 index 0000000000..94dc7a09a9 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/SearchBarToggleTransformer.kt @@ -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 { + + override fun transform(prevState: ChooseTokenInitialUM): ChooseTokenInitialUM { + return prevState.copy( + searchBar = prevState.searchBar.copy(isActive = isActive), + ) + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/SearchBarUpdateQueryTransformer.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/SearchBarUpdateQueryTransformer.kt new file mode 100644 index 0000000000..33cf694057 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/SearchBarUpdateQueryTransformer.kt @@ -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 { + + override fun transform(prevState: ChooseTokenInitialUM): ChooseTokenInitialUM { + return prevState.copy( + searchBar = prevState.searchBar.copy(query = newQuery), + ) + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt index 7d7812024f..2eb003236d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt @@ -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.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() private val bridge: ChooseTokenBridge = params.bridge - private val searchQueryState = bridge.searchQueryState - private val addToPortfolioJobHolder = JobHolder() + private val searchQueryState: StateFlow = bridge.searchQueryState + private val isSearchingState: Boolean get() = bridge.searchQueryState.isSearchingState + private val marketBlockDelegate: MarketBlockDelegate = marketBlockDelegateFactory.create( + modelScope = modelScope, + searchQueryState = searchQueryState, + screensSourcesName = params.analyticsPayload + .filterIsInstance() + .firstOrNull()?.value.orEmpty(), + ) + private val portfolioListBlockDelegate: PortfolioListBlockDelegate = portfolioListBlockDelegateFactory.create( + modelScope = modelScope, + searchQueryState = searchQueryState, + ) - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - - private val visibleMarketItemIds = MutableStateFlow>(emptyList()) - private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) + val bottomSheetNavigation get() = marketBlockDelegate.addToPortfolioSlot + val addToPortfolioManager get() = marketBlockDelegate.addToPortfolioManager + private val marketsStateFlow: Flow = if (params.settings.isShowMarketBlock) { + marketBlockDelegate.marketsStateFlow + } else { + flowOf(null) + } private val expandedAccountsFlow: MutableStateFlow> = MutableStateFlow(emptyMap()) + private val onWalletSelected = Channel() - 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 = 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 = combineUI() + private val initialState: MutableStateFlow = MutableStateFlow(getInitState()) + val state: StateFlow = 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 = combineUI() - - init { - subscribeMarketTokens() - } - - private fun combineUI(): StateFlow = combine( + @Suppress("UnusedPrivateMember") + private fun combineUIOld(): StateFlow = 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 = channelFlow { + val allWalletsFlow: StateFlow> = + 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 = + 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 = combine( + flow = selectedWalletFlow.map { wallet -> wallet.walletId }.distinctUntilChanged(), + flow2 = portfolioListBlockDelegate.portfolioList, + transform = { selectedWalletId, allPortfoliosData -> allPortfoliosData[selectedWalletId] }, + ) + .filterNotNull() .distinctUntilChanged() - .transformLatest, 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, 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 { - 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 { - 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 } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt new file mode 100644 index 0000000000..d83753ba54 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt @@ -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, + @Assisted private val screensSourcesName: String, +) { + + private val addToPortfolioJobHolder = JobHolder() + private val visibleMarketItemIds = MutableStateFlow>(emptyList()) + private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) + + val addToPortfolioSlot: SlotNavigation = SlotNavigation() + var addToPortfolioManager: AddToPortfolioManager? = null + + val marketsStateFlow: Flow = 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, Unit> { visibleBatchKeys -> + searchMarketsListManager.loadCharts(visibleBatchKeys) + } + .launchIn(modelScope) + + visibleDefaultMarketItemIds + .mapNotNull { rawIds -> + if (rawIds.isNotEmpty()) { + defaultMarketsListManager.getBatchKeysByItemIds(rawIds) + } else { + null + } + }.distinctUntilChanged() + .transformLatest, Unit> { visibleBatchKeys -> + defaultMarketsListManager.loadCharts(visibleBatchKeys) + } + .launchIn(modelScope) + } + + private fun createDefaultMarketsFlow(): Flow { + 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 { + 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, + modelScope: CoroutineScope, + screensSourcesName: String, + ): MarketBlockDelegate + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/PortfolioListBlockDelegate.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/PortfolioListBlockDelegate.kt new file mode 100644 index 0000000000..64558e8d15 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/PortfolioListBlockDelegate.kt @@ -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, +) : ClickIntents { + + val onTokenItemClick: Channel> = Channel() + + val portfolioList: Flow> = flow { + val allAccountsFlow: Flow> = + multiAccountStatusListSupplier.invokeAsMap() + + val allWalletsFlow: Flow> = + 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.toExpandedAccountsMap(): Flow>> { + if (isEmpty()) return flowOf(emptyMap()) + val flows: List>>> = + 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, modelScope: CoroutineScope): PortfolioListBlockDelegate + } +} + +internal interface ClickIntents { + fun onTokenItemClick(account: AccountStatus, currencyStatus: CryptoCurrencyStatus) + + fun onAccountExpandClick(account: Account) + + fun onAccountCollapseClick(account: Account) +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt index d40b8c87ae..626a24fe4e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt @@ -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, 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 { - override val values: Sequence = 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 { + override val values: Sequence = 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, ), ) } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt index 8b9b2e3c23..3f23f9cd85 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt @@ -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, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt index 7303bfbb6b..d30b9a4165 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt @@ -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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index ebfe4ac052..35b381f033 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 1ad54a1a82..b9bbb76f34 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -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 { - return groups.fold(initial = persistentListOf()) { acc, group -> - acc.mutate { it.addGroup(tokenConverter, group) } - } - } - - private fun TokenList.Ungrouped.toUngroupedItems( - tokenConverter: TokenItemStateConverter, - ): PersistentList { - return currencies.fold(initial = persistentListOf()) { acc, token -> - acc.mutate { it.addToken(tokenConverter, token) } - } - } - - private fun MutableList.addGroup( - tokenConverter: TokenItemStateConverter, - group: NetworkGroup, - ): List { - 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.addToken( - tokenConverter: TokenItemStateConverter, - token: CryptoCurrencyStatus, - ): List { - 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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt index a94d6b5b78..95fa71713d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt @@ -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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index 54b8d8deb9..6192849dc4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -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, appCurrency: AppCurrency, - accountId: AccountId, + mainAccount: AccountStatus.CryptoPortfolio, yieldSupplyApyMap: Map = emptyMap(), stakingAvailabilityMap: Map = 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, diff --git a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt index cbdb878996..2df2e0c850 100644 --- a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt +++ b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt @@ -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(