Updated on 2026-08-14
This commit is contained in:
parent
e49dc700ac
commit
e3f6d16441
16 changed files with 597 additions and 118 deletions
|
|
@ -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<AppCurrency>,
|
||||
private val currentSearchText: Provider<String?>,
|
||||
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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<MarketsListItemUM>,
|
||||
val total: Int,
|
||||
val loadMore: () -> Unit,
|
||||
val onItemClick: (MarketsListItemUM) -> Unit,
|
||||
val visibleIdsChanged: (List<CryptoCurrency.RawID>) -> 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String> = 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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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<Throwable, List<SwapPairLeast>>
|
|||
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<List<CryptoCurrency.RawID>>(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<List<CryptoCurrency.RawID>>(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<SwapMarketState> {
|
||||
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<SwapMarketState> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ sealed class SwapEvents(
|
|||
params: Map<String, String> = 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,
|
||||
|
|
|
|||
|
|
@ -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<SwapProvider, SwapState.QuotesLoadedState>
|
||||
|
||||
@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<List<CryptoCurrency.RawID>>(emptyList())
|
||||
private val visibleDefaultMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(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<SwapProvider>.filterForTangemPayWithdrawal(): List<SwapProvider> {
|
||||
return if (tangemPayInput?.isWithdrawal == true) {
|
||||
filter { it.type == ExchangeProviderType.CEX }
|
||||
|
|
@ -2058,9 +2149,73 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createDefaultMarketsFlow(): Flow<SwapMarketState> {
|
||||
val marketsTitle = TextReference.Res(R.string.feed_trending_now)
|
||||
return combine(
|
||||
defaultMarketsListManager.uiItems,
|
||||
defaultMarketsListManager.isInInitialLoadingErrorState,
|
||||
defaultMarketsListManager.totalCount,
|
||||
) { uiItems, isError, total ->
|
||||
when {
|
||||
isError -> SwapMarketState.LoadingError(
|
||||
onRetryClicked = { defaultMarketsListManager.reload() },
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = false,
|
||||
)
|
||||
uiItems.isEmpty() -> SwapMarketState.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<SwapMarketState> {
|
||||
val marketsTitle = TextReference.Res(R.string.markets_common_title)
|
||||
return combine(
|
||||
flow = searchMarketsListManager.uiItems,
|
||||
flow2 = searchMarketsListManager.isInInitialLoadingErrorState,
|
||||
flow3 = searchMarketsListManager.isSearchNotFoundState,
|
||||
flow4 = searchMarketsListManager.totalCount,
|
||||
) { uiItems, isError, isSearchNotFound, total ->
|
||||
when {
|
||||
isError -> SwapMarketState.LoadingError(
|
||||
onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value) },
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = true,
|
||||
)
|
||||
isSearchNotFound -> SwapMarketState.SearchNothingFound
|
||||
uiItems.isEmpty() -> SwapMarketState.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 }
|
||||
|
|
|
|||
|
|
@ -64,6 +64,12 @@ internal class SwapNotificationsFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun getSwapNotSupportedNotifications(tokenName: String): ImmutableList<NotificationUM> {
|
||||
return persistentListOf(
|
||||
SwapNotificationUM.Warning.SwapNotSupported(tokenName),
|
||||
)
|
||||
}
|
||||
|
||||
fun getQuotesErrorStateNotifications(
|
||||
expressDataError: ExpressDataError,
|
||||
fromToken: CryptoCurrency,
|
||||
|
|
|
|||
|
|
@ -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<AppCurrency>,
|
||||
private val currentSearchText: Provider<String?>,
|
||||
private val modelScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
private val actionsFlow = MutableSharedFlow<BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>>()
|
||||
private val actionsFlow =
|
||||
MutableSharedFlow<BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>>(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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<MarketsListItemUM>,
|
||||
val total: Int,
|
||||
val loadMore: () -> Unit,
|
||||
val onItemClick: (MarketsListItemUM) -> Unit,
|
||||
val visibleIdsChanged: (List<CryptoCurrency.RawID>) -> 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue