diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt index 8d9b34c5b8..5df10fda71 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.* internal class SwapMarketsListBatchFlowManager( getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, + private val order: TokenMarketListConfig.Order, private val currentAppCurrency: Provider, private val currentSearchText: Provider, private val modelScope: CoroutineScope, @@ -183,7 +184,7 @@ internal class SwapMarketsListBatchFlowManager( searchText ?: currentSearchText() }, priceChangeInterval = TokenMarketListConfig.Interval.H24, - order = TokenMarketListConfig.Order.ByRating, + order = order, shouldNetworks = true, ), ), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt index bcd1c46b86..7e9abb3982 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt @@ -2,25 +2,40 @@ package com.tangem.features.onramp.swap.availablepairs.market.state import androidx.compose.runtime.Immutable 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.models.currency.CryptoCurrency import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class SwapMarketState { + abstract val marketsTitle: TextReference + abstract val shouldAssetsCount: Boolean + data class Content( val items: ImmutableList, val total: Int, val loadMore: () -> Unit, val onItemClick: (MarketsListItemUM) -> Unit, val visibleIdsChanged: (List) -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, ) : SwapMarketState() - data object Loading : SwapMarketState() + data class Loading( + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, + ) : SwapMarketState() data class LoadingError( val onRetryClicked: () -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, ) : SwapMarketState() - data object SearchNothingFound : SwapMarketState() + data object SearchNothingFound : SwapMarketState() { + override val marketsTitle: TextReference = TextReference.Res(R.string.markets_common_title) + override val shouldAssetsCount: Boolean = true + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt new file mode 100644 index 0000000000..f5a57df2b5 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt @@ -0,0 +1,44 @@ +package com.tangem.features.onramp.swap.availablepairs.model + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM + +private const val SWAP_CATEGORY = "Swap" + +internal sealed class AvailableSwapPairsAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(SWAP_CATEGORY, event, params) { + + class TokenSelected( + val token: String, + val source: String, + val isSearched: Boolean, + ) : AvailableSwapPairsAnalyticsEvent( + event = "Token Selected", + params = mapOf( + TOKEN_PARAM to token, + SOURCE to source, + SEARCHED to if (isSearched) "True" else "False", + ), + ) { + companion object { + const val SOURCE = "Source" + const val SEARCHED = "Searched" + const val SOURCE_PORTFOLIO = "Portfolio" + const val SOURCE_MARKETS = "Markets" + } + } + + class TokenAdded( + val token: String, + val blockchain: String, + ) : AvailableSwapPairsAnalyticsEvent( + event = "Token Added", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index 7867772800..62822d4108 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -1,8 +1,12 @@ package com.tangem.features.onramp.swap.availablepairs.model +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.fields.InputManager +import com.tangem.core.ui.R as CoreUiR +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList @@ -22,6 +26,7 @@ import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading 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.account.Account import com.tangem.domain.models.account.AccountStatus @@ -66,6 +71,7 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.runSuspendCatching +import kotlinx.coroutines.ExperimentalCoroutinesApi import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -77,6 +83,7 @@ private typealias AvailablePairsState = Lce> internal class AvailableSwapPairsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, private val getTokenListUseCase: GetTokenListUseCase, private val tokenListUMController: TokenListUMController, private val searchManager: InputManager, @@ -120,10 +127,23 @@ internal class AvailableSwapPairsModel @Inject constructor( private val searchQueryStateForMarkets = MutableStateFlow("") private val visibleMarketItemIds = MutableStateFlow>(emptyList()) + private val defaultMarketsListManager by lazy { + SwapMarketsListBatchFlowManager( + getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, + order = TokenMarketListConfig.Order.Trending, + currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, + currentSearchText = Provider { null }, + modelScope = modelScope, + dispatchers = dispatchers, + ) + } + private val searchMarketsListManager by lazy { SwapMarketsListBatchFlowManager( getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, + order = TokenMarketListConfig.Order.ByRating, currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, currentSearchText = Provider { searchQueryStateForMarkets.value }, modelScope = modelScope, @@ -131,6 +151,8 @@ internal class AvailableSwapPairsModel @Inject constructor( ) } + private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) + init { if (accountsFeatureToggles.isFeatureEnabled) { subscribeOnUpdateStateV2() @@ -298,7 +320,7 @@ internal class AvailableSwapPairsModel @Inject constructor( } else { UpdateTokenItemsTransformer( appCurrency = appCurrency, - onItemClick = params.onTokenClick, + onItemClick = ::onPortfolioTokenClick, statuses = filterByQueryTokenList.filterByAvailability(availablePairs = availablePairs), isBalanceHidden = isBalanceHidden, unavailableTokensHeaderReference = resourceReference( @@ -357,7 +379,7 @@ internal class AvailableSwapPairsModel @Inject constructor( } else { UpdateAccountTokenListTransformer( appCurrency = appCurrency, - onItemClick = params.onTokenClick, + onItemClick = ::onPortfolioTokenClick, accountList = filterByQueryAccountList.filterByAvailability(availablePairs = availablePairs), isBalanceHidden = isBalanceHidden, unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), @@ -543,6 +565,17 @@ internal class AvailableSwapPairsModel @Inject constructor( } } + private fun onPortfolioTokenClick(tokenItem: TokenItemState, status: CryptoCurrencyStatus) { + analyticsEventHandler.send( + AvailableSwapPairsAnalyticsEvent.TokenSelected( + token = status.currency.symbol, + source = AvailableSwapPairsAnalyticsEvent.TokenSelected.SOURCE_PORTFOLIO, + isSearched = state.value.searchBarUM.query.isNotEmpty(), + ), + ) + params.onTokenClick(tokenItem, status) + } + private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo { return LeastTokenInfo( contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", @@ -550,50 +583,121 @@ internal class AvailableSwapPairsModel @Inject constructor( ) } - @Suppress("LongMethod") + @OptIn(ExperimentalCoroutinesApi::class) private fun subscribeOnMarketsUpdates() { - combine( - flow = searchQueryStateForMarkets - .onEach { searchQuery -> - if (searchQuery.isNotEmpty()) { - searchMarketsListManager.reload(searchQuery) - } - }, - flow2 = searchMarketsListManager.uiItems, - flow3 = searchMarketsListManager.isInInitialLoadingErrorState, - flow4 = searchMarketsListManager.isSearchNotFoundState, - flow5 = searchMarketsListManager.totalCount, - ) { searchQuery, uiItems, isError, isSearchNotFound, total -> - when { - searchQuery.isEmpty() -> { + searchQueryStateForMarkets + .map { it.isEmpty() } + .distinctUntilChanged() + .flatMapLatest { isDefaultMode -> + if (isDefaultMode) { visibleMarketItemIds.value = emptyList() - null + createDefaultMarketsFlow() + } else { + visibleDefaultMarketItemIds.value = emptyList() + createSearchMarketsFlow() } + } + .onEach { marketsState -> + tokenListUMController.update { it.copy(marketsState = marketsState) } + } + .flowOn(dispatchers.main) + .launchIn(modelScope) + + searchQueryStateForMarkets + .onEach { searchQuery -> + if (searchQuery.isNotEmpty()) { + searchMarketsListManager.reload(searchQuery) + } + } + .launchIn(modelScope) + + params.selectedStatus + .filterNotNull() + .take(1) + .onEach { defaultMarketsListManager.reload() } + .launchIn(modelScope) + } + + private fun createDefaultMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(CoreUiR.string.feed_trending_now) + return combine( + defaultMarketsListManager.uiItems, + defaultMarketsListManager.isInInitialLoadingErrorState, + defaultMarketsListManager.totalCount, + ) { uiItems, isError, total -> + when { isError -> SwapMarketState.LoadingError( - onRetryClicked = { searchMarketsListManager.reload(searchQuery) }, + onRetryClicked = { defaultMarketsListManager.reload() }, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + uiItems.isEmpty() -> SwapMarketState.Loading( + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + 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(CoreUiR.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(searchQueryStateForMarkets.value) + }, + marketsTitle = marketsTitle, + shouldAssetsCount = true, ) isSearchNotFound -> SwapMarketState.SearchNothingFound - uiItems.isEmpty() -> SwapMarketState.Loading + uiItems.isEmpty() -> SwapMarketState.Loading( + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) else -> SwapMarketState.Content( items = uiItems, loadMore = { searchMarketsListManager.loadMore() }, onItemClick = { item -> addToPortfolioItem(item) }, visibleIdsChanged = { visibleMarketItemIds.value = it }, total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = true, ) } } - .distinctUntilChanged() - .onEach { marketsState -> - tokenListUMController.update { it.copy(marketsState = marketsState) } - } - .flowOn(dispatchers.main) - .launchIn(modelScope) } private fun onTokenAddedToPortfolio(addedToken: CryptoCurrency) { modelScope.launch { bottomSheetNavigation.dismiss() + analyticsEventHandler.send( + AvailableSwapPairsAnalyticsEvent.TokenAdded( + token = addedToken.symbol, + blockchain = addedToken.network.name, + ), + ) + analyticsEventHandler.send( + AvailableSwapPairsAnalyticsEvent.TokenSelected( + token = addedToken.symbol, + source = AvailableSwapPairsAnalyticsEvent.TokenSelected.SOURCE_MARKETS, + isSearched = state.value.searchBarUM.query.isNotEmpty(), + ), + ) // Trigger re-fetch of available pairs (clears cache + re-enters collectLatest) refreshPairsTrigger.emit(Unit) @@ -615,7 +719,9 @@ internal class AvailableSwapPairsModel @Inject constructor( private fun addToPortfolioItem(item: MarketsListItemUM) { modelScope.launch { - val tokenMarket = searchMarketsListManager.getTokenMarketById(item.id) ?: return@launch + val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) + ?: searchMarketsListManager.getTokenMarketById(item.id) + ?: return@launch val param = tokenMarket.toSerializableParam() val hasOnlyHotWallets = allUserWallets.all { it is UserWallet.Hot } @@ -663,5 +769,16 @@ internal class AvailableSwapPairsModel @Inject constructor( searchMarketsListManager.loadCharts(visibleBatchKeys) } } + modelScope.launch { + visibleDefaultMarketItemIds.mapNotNull { rawIds -> + if (rawIds.isNotEmpty()) { + defaultMarketsListManager.getBatchKeysByItemIds(rawIds) + } else { + null + } + }.distinctUntilChanged().collectLatest { visibleBatchKeys -> + defaultMarketsListManager.loadCharts(visibleBatchKeys) + } + } } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt index b8e098fa31..18600da888 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt @@ -17,6 +17,7 @@ import com.tangem.common.ui.markets.MarketsListItemPlaceholder import com.tangem.core.ui.R import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState @@ -28,7 +29,7 @@ internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) { val totalCount = (state as? SwapMarketState.Content)?.total Text( text = buildAnnotatedString { - append(stringResourceSafe(R.string.markets_common_title)) + append(state.marketsTitle.resolveReference()) if (totalCount != null) { withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { append(" $totalCount") diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index 6cecf0ac0e..d92870b44f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -50,8 +50,7 @@ import kotlinx.collections.immutable.ImmutableList * */ internal fun LazyListScope.onrampSwapTokenList(state: TokenListUM) { - val isSearchMode = state.searchBarUM.query.isNotEmpty() && state.marketsState != null - if (isSearchMode) { + if (state.marketsState != null) { onrampTokenListWithMarkets(state = state) } else { onrampTokenList(state = state) @@ -91,7 +90,10 @@ private fun LazyListScope.onrampTokenListWithMarkets(state: TokenListUM) { state.tokensListData.totalTokensCount != 0 if (hasAssets) { - assetsTitle(count = state.tokensListData.totalTokensCount) + assetsTitle( + count = state.tokensListData.totalTokensCount, + showCount = state.marketsState?.shouldAssetsCount == true, + ) tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) @@ -159,13 +161,15 @@ private fun LazyListScope.searchBarItem(searchBarUM: SearchBarUM, modifier: Modi } } -private fun LazyListScope.assetsTitle(count: Int) { +private fun LazyListScope.assetsTitle(count: Int, showCount: Boolean) { item(key = "assets_title") { Text( text = buildAnnotatedString { append(stringResourceSafe(R.string.swap_your_assets_title)) - withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { - append(" $count") + if (showCount) { + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(" $count") + } } }, style = TangemTheme.typography.h3, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 98955f6373..cd44e2f95c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -25,7 +25,7 @@ sealed class SwapEvents( params: Map = emptyMap(), ) : AnalyticsEvent(SWAP_CATEGORY, event, params) { - data class SwapScreenOpened( + class SwapScreenOpened( val token: String, val blockchain: String, ) : SwapEvents( @@ -38,25 +38,47 @@ sealed class SwapEvents( class SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") - data class ChooseTokenScreenOpened(val hasAvailableTokens: Boolean) : SwapEvents( + class ChooseTokenScreenOpened(val hasAvailableTokens: Boolean) : SwapEvents( event = "Choose Token Screen Opened", params = mapOf("Available tokens" to if (hasAvailableTokens) "Yes" else "No"), ) - data class ChooseTokenScreenResult(val isTokenChosen: Boolean, val token: String? = null) : SwapEvents( + class ChooseTokenScreenResult( + val isTokenChosen: Boolean, + val token: String? = null, + val source: String? = null, + val isSearched: Boolean? = null, + ) : SwapEvents( event = "Choose Token Screen Result", params = buildMap { put("Token Chosen", if (isTokenChosen) "Yes" else "No") token?.let { put("Token", it) } + source?.let { put(TOKEN_SELECTED_SOURCE, it) } + isSearched?.let { put(SEARCHED, if (it) "True" else "False") } }, + ) { + companion object { + const val TOKEN_SELECTED_SOURCE = "Token Selected Source" + const val SEARCHED = "Searched" + const val SOURCE_PORTFOLIO = "Portfolio" + const val SOURCE_MARKETS = "Markets" + } + } + + class TokenAdded(val token: String, val blockchain: String) : SwapEvents( + event = "Token Added", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), ) - data class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents( + class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents( event = "Button - Swap", params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken), ) - data class ButtonGivePermissionClicked( + class ButtonGivePermissionClicked( val sendToken: String, val receiveToken: String, val provider: SwapProvider, @@ -69,7 +91,7 @@ sealed class SwapEvents( ), ) - data class ButtonPermissionApproveClicked( + class ButtonPermissionApproveClicked( val sendToken: String, val receiveToken: String, val approveType: ApproveType, @@ -88,8 +110,8 @@ sealed class SwapEvents( class ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") - @Suppress("NullableToStringCall") - data class SwapInProgressScreen( + @Suppress("NullableToStringCall", "LongParameterList") + class SwapInProgressScreen( val provider: SwapProvider, val commission: FeeType, // Market / Fast val sendBlockchain: String, @@ -118,24 +140,39 @@ sealed class SwapEvents( class ProviderClicked : SwapEvents("Provider Clicked") - data class ProviderChosen(val provider: SwapProvider) : SwapEvents( + class ProviderChosen(val provider: SwapProvider) : SwapEvents( event = "Provider Chosen", params = mapOf("Provider" to provider.name), ) - data class ButtonStatus(val token: String) : SwapEvents( + class ButtonStatus(val token: String) : SwapEvents( event = "Button - Status", params = mapOf("Token" to token), ) - data class ButtonExplore(val token: String) : SwapEvents( + class ButtonExplore(val token: String) : SwapEvents( event = "Button - Explore", params = mapOf("Token" to token), ) class NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap") - data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents( + class NoticeUnavailableToSwapPair( + val sendToken: String, + val receiveToken: String, + val sendBlockchain: String, + val receiveBlockchain: String, + ) : SwapEvents( + event = "Notice - Unavailable To Swap Pair", + params = mapOf( + SEND_TOKEN to sendToken, + RECEIVE_TOKEN to receiveToken, + "Send Blockchain" to sendBlockchain, + "Receive Blockchain" to receiveBlockchain, + ), + ) + + class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents( event = "Notice - Not Enough Fee", params = mapOf( "Token" to token, @@ -143,7 +180,7 @@ sealed class SwapEvents( ), ) - data class NoticeProviderError( + class NoticeProviderError( val sendToken: String, val receiveToken: String, val provider: SwapProvider, @@ -162,7 +199,7 @@ sealed class SwapEvents( // TODO parameters // region Promo activity - data class ChangellyActivity( + class ChangellyActivity( val promoState: PromoState, ) : AnalyticsEvent( category = PROMO_CATEGORY, @@ -177,7 +214,7 @@ sealed class SwapEvents( } } - data class NoticePermissionNeeded( + class NoticePermissionNeeded( val sendToken: String, val receiveToken: String, val provider: SwapProvider, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 35c5fd4fa3..a90bd3640a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue + import arrow.core.Either import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation @@ -44,6 +45,7 @@ import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType 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.account.Account import com.tangem.domain.models.currency.CryptoCurrency @@ -88,7 +90,7 @@ import com.tangem.feature.swap.models.UiActions import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager import com.tangem.feature.swap.models.market.state.SwapMarketState import com.tangem.feature.swap.models.states.SwapNotificationUM -import com.tangem.feature.swap.presentation.R +import com.tangem.core.ui.R import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder @@ -105,6 +107,7 @@ import com.tangem.utils.Provider import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* @@ -120,6 +123,7 @@ import javax.inject.Inject typealias SuccessLoadedSwapData = Map +@OptIn(ExperimentalCoroutinesApi::class) @Suppress("LongParameterList", "LargeClass") @Stable @ModelScoped @@ -154,7 +158,7 @@ internal class SwapModel @Inject constructor( private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, private val sendFeatureToggles: SendFeatureToggles, private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, - swapFeatureToggles: SwapFeatureToggles, + private val swapFeatureToggles: SwapFeatureToggles, private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, private val excludedBlockchains: ExcludedBlockchains, private val getUserWalletsUseCase: GetWalletsUseCase, @@ -258,10 +262,26 @@ internal class SwapModel @Inject constructor( private val searchQueryState = MutableStateFlow("") private val visibleMarketItemIds = MutableStateFlow>(emptyList()) + private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) + private var latestMarketsState: SwapMarketState? = null + + private val defaultMarketsListManager by lazy { + SwapMarketsListBatchFlowManager( + getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, + order = TokenMarketListConfig.Order.Trending, + currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, + currentSearchText = Provider { null }, + modelScope = modelScope, + dispatchers = dispatchers, + ) + } + private val searchMarketsListManager by lazy { SwapMarketsListBatchFlowManager( getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, + order = TokenMarketListConfig.Order.ByRating, currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, currentSearchText = Provider { searchQueryState.value }, modelScope = modelScope, @@ -279,13 +299,21 @@ internal class SwapModel @Inject constructor( override fun onSuccess(addedToken: CryptoCurrency) { modelScope.launch { bottomSheetNavigation.dismiss() - uiState.selectTokenState?.let { currentSelectState -> - uiState = uiState.copy( - selectTokenState = currentSelectState.copy( - marketsState = null, - ), - ) - } + analyticsEventHandler.send( + SwapEvents.ChooseTokenScreenResult( + isTokenChosen = true, + token = addedToken.symbol, + source = SwapEvents.ChooseTokenScreenResult.SOURCE_MARKETS, + isSearched = searchQueryState.value.isNotEmpty(), + ), + ) + analyticsEventHandler.send( + SwapEvents.TokenAdded( + token = addedToken.symbol, + blockchain = addedToken.network.name, + ), + ) + searchQueryState.value = "" getAccountCurrencyStatusUseCase.invoke(userWalletId, addedToken) .firstOrNull { it.status.value is CryptoCurrencyStatus.Loaded @@ -362,50 +390,7 @@ internal class SwapModel @Inject constructor( } .launchIn(modelScope) - if (swapFeatureToggles.isMarketListFeatureEnabled) { - combine( - flow = searchQueryState - .onEach { searchQuery -> - searchMarketsListManager.reload(searchQuery) - }, - flow2 = searchMarketsListManager.uiItems, - flow3 = searchMarketsListManager.isInInitialLoadingErrorState, - flow4 = searchMarketsListManager.isSearchNotFoundState, - flow5 = searchMarketsListManager.totalCount.filterNotNull(), - ) { searchQuery, uiItems, isError, isSearchNotFound, total -> - when { - searchQuery.isEmpty() -> { - visibleMarketItemIds.value = emptyList() - null - } - isError -> SwapMarketState.LoadingError( - onRetryClicked = { searchMarketsListManager.reload(searchQuery) }, - ) - isSearchNotFound -> SwapMarketState.SearchNothingFound - uiItems.isEmpty() -> SwapMarketState.Loading - else -> SwapMarketState.Content( - items = uiItems, - loadMore = { searchMarketsListManager.loadMore() }, - onItemClick = { item -> - addToPortfolioItem(item) - }, - visibleIdsChanged = { visibleMarketItemIds.value = it }, - total = total, - ) - } - } - .distinctUntilChanged() - .onEach { marketsState -> - uiState.selectTokenState?.let { currentSelectState -> - uiState = uiState.copy( - selectTokenState = currentSelectState.copy( - marketsState = marketsState, - ), - ) - } - } - .launchIn(modelScope) - } + subscribeMarketTokens() modelScope.launch { visibleMarketItemIds.mapNotNull { rawIDS -> @@ -418,6 +403,18 @@ internal class SwapModel @Inject constructor( searchMarketsListManager.loadCharts(visibleBatchKeys) } } + + modelScope.launch { + visibleDefaultMarketItemIds.mapNotNull { rawIds -> + if (rawIds.isNotEmpty()) { + defaultMarketsListManager.getBatchKeysByItemIds(rawIds) + } else { + null + } + }.distinctUntilChanged().collectLatest { visibleBatchKeys -> + defaultMarketsListManager.loadCharts(visibleBatchKeys) + } + } } fun onStart() { @@ -443,6 +440,41 @@ internal class SwapModel @Inject constructor( analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(hasAvailableTokens = isAnyAvailableTokens)) } + private fun subscribeMarketTokens() { + if (swapFeatureToggles.isMarketListFeatureEnabled) { + // Switch between default and search market flows + searchQueryState + .map { it.isEmpty() } + .distinctUntilChanged() + .flatMapLatest { isDefaultMode -> + if (isDefaultMode) { + visibleMarketItemIds.value = emptyList() + createDefaultMarketsFlow() + } else { + visibleDefaultMarketItemIds.value = emptyList() + createSearchMarketsFlow() + } + } + .onEach { marketsState -> + latestMarketsState = marketsState + applyMarketsState(marketsState) + } + .launchIn(modelScope) + + // Reload search markets when query changes + searchQueryState + .onEach { searchQuery -> + if (searchQuery.isNotEmpty()) { + searchMarketsListManager.reload(searchQuery) + } + } + .launchIn(modelScope) + + // Initial load of default markets + defaultMarketsListManager.reload() + } + } + @Suppress("LongMethod") private fun initTokens(isReverseFromTo: Boolean) { modelScope.launch(dispatchers.main) { @@ -613,6 +645,25 @@ internal class SwapModel @Inject constructor( toAccount = toAccount, tokensDataState = state, ) + + if (!isTokenAvailableForSwap(state, selectedCurrency, isReverseFromTo)) { + analyticsEventHandler.send( + SwapEvents.NoticeUnavailableToSwapPair( + sendToken = fromCurrencyStatus.currency.symbol, + receiveToken = toCurrencyStatus.currency.symbol, + sendBlockchain = fromCurrencyStatus.currency.network.name, + receiveBlockchain = toCurrencyStatus.currency.network.name, + ), + ) + uiState = stateBuilder.createSwapNotSupportedState( + uiStateHolder = uiState, + fromToken = fromCurrencyStatus, + toToken = toCurrencyStatus, + toAccount = toAccount, + ) + return + } + startLoadingQuotes( fromToken = fromCurrencyStatus, fromAccount = fromAccount, @@ -640,6 +691,17 @@ internal class SwapModel @Inject constructor( fromToken = dataState.fromCryptoCurrency?.currency ?: initialCurrencyFrom, ) } + latestMarketsState?.let(::applyMarketsState) + } + + private fun applyMarketsState(marketsState: SwapMarketState) { + uiState.selectTokenState?.let { currentSelectState -> + uiState = uiState.copy( + selectTokenState = currentSelectState.copy( + marketsState = marketsState, + ), + ) + } } private fun startLoadingQuotes( @@ -1275,7 +1337,14 @@ internal class SwapModel @Inject constructor( val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id) foundToken?.currency?.symbol?.let { symbol -> - analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol)) + analyticsEventHandler.send( + SwapEvents.ChooseTokenScreenResult( + isTokenChosen = true, + token = symbol, + source = SwapEvents.ChooseTokenScreenResult.SOURCE_PORTFOLIO, + isSearched = searchQueryState.value.isNotEmpty(), + ), + ) } if (foundToken != null) { @@ -1871,6 +1940,28 @@ internal class SwapModel @Inject constructor( .orEmpty() } + private fun isTokenAvailableForSwap( + state: TokensDataStateExpress, + selectedCurrency: CryptoCurrencyStatus, + isReverseFromTo: Boolean, + ): Boolean { + val group = if (isReverseFromTo) state.fromGroup else state.toGroup + val idToFind = selectedCurrency.currency.id.value + + return if (accountsFeatureToggles.isFeatureEnabled) { + group.accountCurrencyList.any { (_, currencyList) -> + currencyList.any { accountSwapCurrency -> + idToFind == accountSwapCurrency.cryptoCurrencyStatus.currency.id.value && + accountSwapCurrency.isAvailable + } + } + } else { + group.available.any { swapAvailability -> + idToFind == swapAvailability.currencyStatus.currency.id.value + } + } + } + private fun List.filterForTangemPayWithdrawal(): List { return if (tangemPayInput?.isWithdrawal == true) { filter { it.type == ExchangeProviderType.CEX } @@ -2058,9 +2149,73 @@ internal class SwapModel @Inject constructor( } } + 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.Loading( + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + 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.Loading( + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + 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 = searchMarketsListManager.getTokenMarketById(item.id) ?: return@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 } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 6c9bd0a484..fdb774f17d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -64,6 +64,12 @@ internal class SwapNotificationsFactory( ) } + fun getSwapNotSupportedNotifications(tokenName: String): ImmutableList { + return persistentListOf( + SwapNotificationUM.Warning.SwapNotSupported(tokenName), + ) + } + fun getQuotesErrorStateNotifications( expressDataError: ExpressDataError, fromToken: CryptoCurrency, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt index e8331efed3..77609fade7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt @@ -22,12 +22,14 @@ import kotlinx.coroutines.flow.* internal class SwapMarketsListBatchFlowManager( getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, + private val order: TokenMarketListConfig.Order, private val currentAppCurrency: Provider, private val currentSearchText: Provider, private val modelScope: CoroutineScope, private val dispatchers: CoroutineDispatcherProvider, ) { - private val actionsFlow = MutableSharedFlow>() + private val actionsFlow = + MutableSharedFlow>(replay = 1) private val updateStateJob = JobHolder() private val batchFlow = getMarketsTokenListFlowUseCase( @@ -183,7 +185,7 @@ internal class SwapMarketsListBatchFlowManager( searchText ?: currentSearchText() }, priceChangeInterval = TokenMarketListConfig.Interval.H24, - order = TokenMarketListConfig.Order.ByRating, + order = order, shouldNetworks = true, ), ), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt index 50e6d14a74..8be842bcfd 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt @@ -2,25 +2,40 @@ package com.tangem.feature.swap.models.market.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.core.ui.R import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class SwapMarketState { + abstract val marketsTitle: TextReference + abstract val shouldAssetsCount: Boolean + data class Content( val items: ImmutableList, val total: Int, val loadMore: () -> Unit, val onItemClick: (MarketsListItemUM) -> Unit, val visibleIdsChanged: (List) -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, ) : SwapMarketState() - data object Loading : SwapMarketState() + data class Loading( + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, + ) : SwapMarketState() data class LoadingError( val onRetryClicked: () -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, ) : SwapMarketState() - data object SearchNothingFound : SwapMarketState() + data object SearchNothingFound : SwapMarketState() { + override val marketsTitle: TextReference = TextReference.Res(R.string.markets_common_title) + override val shouldAssetsCount: Boolean = true + } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt index d140ef1ff0..7e5209dca5 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt @@ -132,6 +132,18 @@ internal object SwapNotificationUM { ), ) + data class SwapNotSupported( + val tokenName: String, + ) : Warning( + title = resourceReference( + id = com.tangem.feature.swap.presentation.R.string.express_swap_not_supported_title, + formatArgs = wrappedList(tokenName), + ), + subtitle = resourceReference( + com.tangem.feature.swap.presentation.R.string.express_swap_not_supported_text, + ), + ) + data class NeedReserveToCreateAccount( val receiveAmount: String, val receiveToken: String, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index c3a85fe555..6c955e7c86 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -200,6 +200,60 @@ internal class StateBuilder( ) } + fun createSwapNotSupportedState( + uiStateHolder: SwapStateHolder, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, + toAccount: Account.CryptoPortfolio?, + ): SwapStateHolder { + if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder + return uiStateHolder.copy( + sendCardData = SwapCardState.SwapCardData( + type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable), + amountTextFieldValue = null, + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + token = fromToken, + tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, + coinId = uiStateHolder.sendCardData.coinId, + isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, + tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, + canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, + balance = fromToken.getFormattedAmount(isNeedSymbol = false), + networkIconRes = getActiveIconRes(fromToken.currency.network.rawId), + isBalanceHidden = isBalanceHiddenProvider(), + ), + receiveCardData = SwapCardState.SwapCardData( + type = TransactionCardType.ReadOnly( + accountTitleUM = getToCardAccountTitle(toAccount), + ), + amountTextFieldValue = TextFieldValue( + text = "0", + ), + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + token = toToken, + tokenIconUrl = toToken.currency.iconUrl, + coinId = toToken.currency.network.backendId, + isNotNativeToken = toToken.currency is CryptoCurrency.Token, + tokenCurrency = toToken.currency.symbol, + canSelectAnotherToken = true, + balance = toToken.getFormattedAmount(isNeedSymbol = false), + networkIconRes = getActiveIconRes(toToken.currency.network.rawId), + isBalanceHidden = isBalanceHiddenProvider(), + ), + notifications = notificationsFactory.getSwapNotSupportedNotifications(toToken.currency.name), + fee = FeeItemState.Empty, + swapButton = SwapButton( + walletInteractionIcon = walletInterationIcon(userWalletProvider()), + isEnabled = false, + isHoldToConfirm = isHoldToConfirmEnabled, + onClick = { }, + ), + changeCardsButtonState = ChangeCardsButtonState.DISABLED, + providerState = ProviderState.Empty(), + priceImpact = PriceImpact.Empty(), + ) + } + @Suppress("LongParameterList") fun createQuotesLoadingState( uiStateHolder: SwapStateHolder, @@ -438,6 +492,7 @@ internal class StateBuilder( notification is SwapNotificationUM.Warning.ExpressError || notification is SwapNotificationUM.Warning.ExpressGeneralError || notification is SwapNotificationUM.Warning.NoAvailableTokensToSwap || + notification is SwapNotificationUM.Warning.SwapNotSupported || notification is SwapNotificationUM.Warning.NeedReserveToCreateAccount || notification is SwapNotificationUM.Info.PermissionNeeded } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 2bc3cc5c61..a6ddf89de1 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -189,7 +189,7 @@ private fun ListOfTokensWithMarkets( state = lazyListState, ) { if (state.tokensListData !is TokenListUMData.EmptyList) { - assetsTitle(count = state.tokensListData.totalTokensCount) + assetsTitle(count = state.tokensListData.totalTokensCount, showCount = marketsState.shouldAssetsCount) } tokensListItems( @@ -197,10 +197,18 @@ private fun ListOfTokensWithMarkets( isBalanceHidden = state.isBalanceHidden, ) - if (state.tokensListData is TokenListUMData.EmptyList) { + tokensToSelectItems(state.availableTokens, state.onTokenSelected) + if (state.unavailableTokens.isNotEmpty()) { item { SpacerH12() } - } else { + tokensToSelectItems(state.unavailableTokens, state.onTokenSelected) + } + + val hasPortfolioContent = state.tokensListData !is TokenListUMData.EmptyList || + state.availableTokens.isNotEmpty() + if (hasPortfolioContent) { item { SpacerH32() } + } else { + item { SpacerH12() } } swapMarketsListItems(marketsState) @@ -242,13 +250,15 @@ private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapM } } -private fun LazyListScope.assetsTitle(count: Int) { +private fun LazyListScope.assetsTitle(count: Int, showCount: Boolean) { item(key = "assets_title") { Text( text = buildAnnotatedString { append(stringResourceSafe(R.string.swap_your_assets_title)) - withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { - append(" $count") + if (showCount) { + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(" $count") + } } }, style = TangemTheme.typography.h3, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt index 0f69e31f8b..ced4fbda3e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt @@ -14,6 +14,7 @@ import com.tangem.common.ui.markets.MarketsListItem import com.tangem.common.ui.markets.MarketsListItemPlaceholder import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.market.state.SwapMarketState @@ -24,7 +25,7 @@ internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) { val totalCount = (state as? SwapMarketState.Content)?.total Text( text = buildAnnotatedString { - append(stringResourceSafe(R.string.markets_common_title)) + append(state.marketsTitle.resolveReference()) if (totalCount != null) { withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { append(" $totalCount") diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt index b9a6ab2d40..8c03c69176 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt @@ -4,8 +4,10 @@ import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.core.ui.R import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenListUMData @@ -35,6 +37,8 @@ internal class SwapSelectTokenPreviewProvider { onItemClick = { }, visibleIdsChanged = { }, total = TOTAL_ITEMS, + marketsTitle = TextReference.Res(R.string.feed_trending_now), + shouldAssetsCount = false, ) private fun createPreviewMarketItems() = listOf(