Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-19 13:20:36 +02:00
parent 8891721b89
commit 916a4a7242
47 changed files with 8 additions and 2802 deletions

View file

@ -4,7 +4,6 @@ import com.tangem.domain.swap.SwapErrorResolver
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.SwapTransactionRepository
import com.tangem.domain.swap.usecase.*
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -19,12 +18,6 @@ import com.tangem.feature.swap.domain.api.SwapRepository as OldSwapRepository
@InstallIn(SingletonComponent::class)
internal object SwapDomainModule {
@Provides
@Singleton
fun provideGetAvailablePairsUseCase(swapRepository: OldSwapRepository): GetAvailablePairsUseCase {
return GetAvailablePairsUseCase(swapRepository = swapRepository)
}
@Provides
@Singleton
fun provideGetSwapSupportedPairsUseCase(

View file

@ -71,7 +71,6 @@ internal class ChildFactory @Inject constructor(
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
private val buyCryptoComponentFactory: BuyCryptoComponent.Factory,
private val sellCryptoComponentFactory: SellCryptoComponent.Factory,
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
private val newWelcomeComponentFactory: NewWelcomeComponent.Factory,
private val storiesComponentFactory: StoriesComponent.Factory,
@ -253,13 +252,6 @@ internal class ChildFactory @Inject constructor(
componentFactory = sellCryptoComponentFactory,
)
}
is AppRoute.SwapCrypto -> {
createComponentChild(
context = context,
params = SwapSelectTokensComponent.Params(userWalletId = route.userWalletId),
componentFactory = swapSelectTokensComponentFactory,
)
}
is AppRoute.Onboarding -> {
createComponentChild(
context = context,

View file

@ -331,11 +331,6 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/sell_crypto/${userWalletId.stringValue}")
@Serializable
data class SwapCrypto(
val userWalletId: UserWalletId,
) : AppRoute(path = "/swap_crypto/${userWalletId.stringValue}")
/**
* Onboarding V2
* @property scanResponse scan response, determines onboarding route by the product type

View file

@ -82,8 +82,6 @@ sealed class MainScreenAnalyticsEvent(
class BuyScreenOpened : MainScreenAnalyticsEvent(event = "Buy Screen Opened")
class SwapScreenOpened : MainScreenAnalyticsEvent(event = "Swap Screen Opened")
class SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened")
data class BuyTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
@ -96,21 +94,6 @@ sealed class MainScreenAnalyticsEvent(
params = mapOf(TOKEN_PARAM to currencySymbol),
)
data class SwapTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
event = "Swap Token Clicked",
params = mapOf(TOKEN_PARAM to currencySymbol),
)
data class ReceiveTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
event = "Receive Token Clicked",
params = mapOf(TOKEN_PARAM to currencySymbol),
)
data class RemoveTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
event = "Remove Button Clicked",
params = mapOf(TOKEN_PARAM to currencySymbol),
)
data class ButtonClose(val source: AnalyticsParam.ScreensSources) : MainScreenAnalyticsEvent(
event = "Button - Close",
params = mapOf(AnalyticsParam.SOURCE to source.value),

View file

@ -15,19 +15,6 @@ sealed class SwapAnalyticsEvent(
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent("Swap", event, params) {
data class TokenSelected(
val token: String,
val source: ScreensSources,
val isSearched: Boolean,
) : SwapAnalyticsEvent(
event = "Token Selected",
params = mapOf(
TOKEN_PARAM to token,
SOURCE to source.value,
SEARCHED to if (isSearched) "True" else "False",
),
)
class FilterProvider(filterType: String) : SwapAnalyticsEvent(
event = "Filter Provider",
params = mapOf(TYPE to filterType),

View file

@ -1,22 +0,0 @@
package com.tangem.features.onramp.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
/**
* Swap select tokens component
*
[REDACTED_AUTHOR]
*/
interface SwapSelectTokensComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Params, SwapSelectTokensComponent>
/**
* Params
*
* @property userWalletId user wallet id
*/
data class Params(val userWalletId: UserWalletId)
}

View file

@ -1,109 +0,0 @@
package com.tangem.features.onramp.swap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.features.onramp.component.SwapSelectTokensComponent
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute
import com.tangem.features.onramp.swap.model.SwapSelectTokensModel
import com.tangem.features.onramp.swap.ui.SwapSelectTokens
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
import com.tangem.features.onramp.tokenlist.entity.OnrampOperation
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Stable
internal class DefaultSwapSelectTokensComponent @AssistedInject constructor(
tokenListComponentFactory: OnrampTokenListComponent.Factory,
availableSwapPairsComponentFactory: AvailableSwapPairsComponent.Factory,
analyticsEventHandler: AnalyticsEventHandler,
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
@Assisted private val appComponentContext: AppComponentContext,
@Assisted private val params: SwapSelectTokensComponent.Params,
) : AppComponentContext by appComponentContext, SwapSelectTokensComponent {
private val model: SwapSelectTokensModel = getOrCreateModel(params)
private val selectFromTokenListComponent: OnrampTokenListComponent = tokenListComponentFactory.create(
context = child(key = "select_from_token_list"),
params = OnrampTokenListComponent.Params(
filterOperation = OnrampOperation.SWAP,
userWalletId = params.userWalletId,
onTokenClick = model::selectFromToken,
),
)
private val selectToTokenListComponent: AvailableSwapPairsComponent = availableSwapPairsComponentFactory.create(
context = child(key = "select_to_token_list"),
params = AvailableSwapPairsComponent.Params(
userWalletId = params.userWalletId,
selectedStatus = model.fromCurrencyStatus,
onTokenClick = model::selectToToken,
),
)
private val bottomSheetSlot = childSlot(
source = selectToTokenListComponent.bottomSheetNavigation,
serializer = AddToPortfolioRoute.serializer(),
key = "add_to_portfolio_bottom_sheet",
handleBackButton = false,
childFactory = { _, context -> bottomSheetChild(context) },
)
init {
analyticsEventHandler.send(event = MainScreenAnalyticsEvent.SwapScreenOpened())
}
@Suppress("UnsafeCallOnNullableType")
private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent {
return addToPortfolioComponentFactory.create(
context = childByContext(componentContext),
params = AddToPortfolioComponent.Params(
addToPortfolioManager = selectToTokenListComponent.addToPortfolioManager,
),
)
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
val fromTokensState by selectFromTokenListComponent.uiState.collectAsStateWithLifecycle()
val toTokensState by selectToTokenListComponent.uiState.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
SwapSelectTokens(
state = state,
selectFromTokenListComponent = selectFromTokenListComponent,
selectFromTokenListState = fromTokensState,
selectToTokenListComponent = selectToTokenListComponent,
selectToTokenListState = toTokensState,
modifier = modifier,
)
bottomSheet.child?.instance?.BottomSheet()
}
@AssistedFactory
interface Factory : SwapSelectTokensComponent.Factory {
override fun create(
context: AppComponentContext,
params: SwapSelectTokensComponent.Params,
): DefaultSwapSelectTokensComponent
}
}

View file

@ -1,37 +0,0 @@
package com.tangem.features.onramp.swap.availablepairs
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.decompose.ComposableListContentComponent
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import kotlinx.coroutines.flow.StateFlow
/** Token list component that present list of available tokens for swap */
@Stable
internal interface AvailableSwapPairsComponent : ComposableListContentComponent<TokenListUM> {
val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute>
val addToPortfolioManager: AddToPortfolioManager
/** Component factory */
interface Factory : ComponentFactory<Params, AvailableSwapPairsComponent>
/**
* Params
*
* @property userWalletId id of multi-currency wallet
* @property selectedStatus flow of selected status
* @property onTokenClick callback for token click
*/
data class Params(
val userWalletId: UserWalletId,
val selectedStatus: StateFlow<CryptoCurrencyStatus?>,
val onTokenClick: (TokenItemState, CryptoCurrencyStatus) -> Unit,
)
}

View file

@ -1,44 +0,0 @@
package com.tangem.features.onramp.swap.availablepairs
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute
import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.ui.onrampSwapTokenList
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.StateFlow
@Stable
internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: AvailableSwapPairsComponent.Params,
) : AvailableSwapPairsComponent, AppComponentContext by context {
private val model: AvailableSwapPairsModel = getOrCreateModel(params)
override val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute> get() = model.bottomSheetNavigation
override val addToPortfolioManager: AddToPortfolioManager get() = model.addToPortfolioManager
override val uiState: StateFlow<TokenListUM>
get() = model.state
override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) {
onrampSwapTokenList(state = uiState)
}
@AssistedFactory
interface Factory : AvailableSwapPairsComponent.Factory {
override fun create(
context: AppComponentContext,
params: AvailableSwapPairsComponent.Params,
): DefaultAvailableSwapPairsComponent
}
}

View file

@ -1,20 +0,0 @@
package com.tangem.features.onramp.swap.availablepairs.di
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
import com.tangem.features.onramp.swap.availablepairs.DefaultAvailableSwapPairsComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AvailableSwapPairsComponentModule {
@Binds
@Singleton
fun bindAvailableSwapPairsComponentFactory(
factory: DefaultAvailableSwapPairsComponent.Factory,
): AvailableSwapPairsComponent.Factory
}

View file

@ -1,20 +0,0 @@
package com.tangem.features.onramp.swap.availablepairs.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface AvailableSwapPairsModelModule {
@Binds
@IntoMap
@ClassKey(AvailableSwapPairsModel::class)
fun bindAvailableSwapPairsModel(model: AvailableSwapPairsModel): Model
}

View file

@ -1,27 +0,0 @@
package com.tangem.features.onramp.swap.availablepairs.entity.transformers
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.feature.swap.domain.models.ExpressException
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import kotlinx.collections.immutable.persistentListOf
/**
[REDACTED_AUTHOR]
*/
internal class SetErrorWarningTransformer(
private val cause: Throwable,
private val onRefresh: () -> Unit,
) : TokenListUMTransformer {
override fun transform(prevState: TokenListUM): TokenListUM {
return prevState.copy(
availableItems = persistentListOf(),
unavailableItems = persistentListOf(),
warning = NotificationUM.Warning.OnrampErrorNotification(
errorCode = (cause as? ExpressException)?.expressDataError?.code?.toString(),
onRefresh = onRefresh,
),
)
}
}

View file

@ -1,28 +0,0 @@
package com.tangem.features.onramp.swap.availablepairs.entity.transformers
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
/**
* Set [statuses] as loading items
*
[REDACTED_AUTHOR]
*/
internal class SetLoadingTokenItemsTransformer(
private val statuses: List<CryptoCurrencyStatus>,
) : TokenListUMTransformer {
override fun transform(prevState: TokenListUM): TokenListUM {
return prevState.copy(
availableItems = LoadingTokenListItemConverter.convertList(
input = statuses.map(CryptoCurrencyStatus::currency),
).toImmutableList(),
unavailableItems = persistentListOf(),
warning = null,
)
}
}

View file

@ -1,66 +0,0 @@
package com.tangem.features.onramp.swap.availablepairs.entity.transformers
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
import com.tangem.common.ui.account.TokensListPortfolioItemConverter
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
internal class SetNoAvailablePairsTransformer(
private val appCurrency: AppCurrency,
private val accountList: Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>>,
private val isBalanceHidden: Boolean,
private val isAccountsMode: Boolean,
private val unavailableErrorText: TextReference,
) : TokenListUMTransformer {
private val unavailableConverter = OnrampTokenItemStateConverterFactory
.createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText)
override fun transform(prevState: TokenListUM): TokenListUM {
val totalTokensCount = accountList.values.sumOf { it.size }
return prevState.copy(
availableItems = persistentListOf(),
unavailableItems = persistentListOf(),
tokensListData = if (isAccountsMode) {
TokenListUMData.AccountList(
tokensList = accountList.map { (account, cryptoCurrencies) ->
TokensListPortfolioItemConverter(
tokenItemUM = AccountCryptoPortfolioItemStateConverter(
appCurrency = appCurrency,
account = account,
onItemClick = null,
).convert(TotalFiatBalance.Failed),
isExpanded = true,
isCollapsable = false,
tokens = unavailableConverter.convertList(cryptoCurrencies)
.map(TokensListItemUM::Token)
.toPersistentList(),
).convert(Unit)
}.toPersistentList(),
totalTokensCount = totalTokensCount,
)
} else {
TokenListUMData.TokenList(
tokensList = accountList.flatMap { (_, cryptoCurrencies) ->
unavailableConverter.convertList(cryptoCurrencies)
.map(TokensListItemUM::Token)
}.toPersistentList(),
totalTokensCount = totalTokensCount,
)
},
isBalanceHidden = isBalanceHidden,
warning = NotificationUM.Warning.SwapNoAvailablePair,
)
}
}

View file

@ -1,252 +0,0 @@
package com.tangem.features.onramp.swap.availablepairs.market
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.*
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.onramp.swap.availablepairs.market.converter.SwapMarketsTokenItemConverter
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@Suppress("LongParameterList")
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 updateStateJob = JobHolder()
private val batchFlow = getMarketsTokenListFlowUseCase(
batchingContext = TokenListBatchingContext(
actionsFlow = actionsFlow,
coroutineScope = modelScope,
),
batchFlowType = batchFlowType,
)
private val resultBatches = MutableStateFlow(ResultBatches())
private val uiBatches = resultBatches.map { it.uiBatches }
val uiItems: StateFlow<ImmutableList<MarketsListItemUM>>
get() = uiBatches
.map { batches ->
batches.asSequence()
.map { it.data }
.flatten()
.toImmutableList()
}
.distinctUntilChanged()
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = persistentListOf(),
)
val isInInitialLoadingErrorState = batchFlow.state
.map { it.status is PaginationStatus.InitialLoadingError }
.distinctUntilChanged()
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = false,
)
val isSearchNotFoundState = batchFlow.state
.map { batchListState ->
currentSearchText().isNullOrEmpty().not() &&
batchListState.status is PaginationStatus.EndOfPagination &&
batchListState.data.isEmpty()
}
.distinctUntilChanged()
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = false,
)
val totalCount: StateFlow<Int?> = batchFlow.state
.map { it.totalCount }
.distinctUntilChanged()
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = null,
)
init {
batchFlow.state
.map { it.data }
.distinctUntilChanged { a, b ->
a.size == b.size &&
a.map { it.key } == b.map { it.key } &&
a.map { it.data }.flatten() == b.map { it.data }.flatten()
}
.onEach {
coroutineScope {
launch {
updateState(it)
}.saveIn(updateStateJob)
}
}
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
private suspend fun updateState(newList: List<Batch<Int, List<TokenMarket>>>, forceUpdate: Boolean = false) =
withContext(dispatchers.default) {
resultBatches.update { resultBatches ->
val items = resultBatches.uiBatches
val previousList = resultBatches.processedItems
val converter = SwapMarketsTokenItemConverter(appCurrency = currentAppCurrency())
if (newList.isEmpty()) {
return@update ResultBatches(processedItems = emptyList())
}
val isInitialLoading =
forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key
val outItems = if (isInitialLoading) {
newList.map { batch ->
Batch(
key = batch.key,
data = converter.convertList(batch.data),
)
}
} else {
if (previousList.size != newList.size) {
val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet())
val newBatches = newList.filter { keysToAdd.contains(it.key) }
items + newBatches.map { batch ->
Batch(
key = batch.key,
data = converter.convertList(batch.data),
)
}
} else {
items.mapIndexed { batchIndex, batch ->
val prevBatch = previousList[batchIndex]
val newBatch = newList[batchIndex]
if (prevBatch == newBatch) return@mapIndexed batch
Batch(
key = batch.key,
data = batch.data.mapIndexed { index, marketsListItemUM ->
val prevItem = prevBatch.data.getOrNull(index)
val newItem = newBatch.data.getOrNull(index)
if (prevItem != null && newItem != null) {
converter.update(prevItem, marketsListItemUM, newItem)
} else {
newItem?.let { converter.convert(it) } ?: marketsListItemUM
}
},
)
}
}
}
currentCoroutineContext().ensureActive()
ResultBatches(
uiBatches = outItems,
processedItems = newList,
)
}
}
fun reload(searchText: String? = null) {
modelScope.launch {
resultBatches.value = ResultBatches()
actionsFlow.emit(
BatchAction.Reload(
requestParams = TokenMarketListConfig(
fiatPriceCurrency = currentAppCurrency().code,
searchText = if (currentSearchText() == null) {
null
} else {
searchText ?: currentSearchText()
},
priceChangeInterval = TokenMarketListConfig.Interval.H24,
order = order,
shouldNetworks = true,
),
),
)
}
}
fun loadMore() {
modelScope.launch {
actionsFlow.emit(BatchAction.LoadMore())
}
}
fun loadCharts(batchKeys: Set<Int>) {
if (batchKeys.isEmpty()) return
modelScope.launch {
val currentData = batchFlow.state.value.data
val alreadyLoadedChartsBatchKeys = currentData
.filter { batch ->
val first = batch.data.firstOrNull() ?: return@filter false
first.tokenCharts.h24 != null
}
.map { it.key }
.toSet()
val batchesKeysToLoad = batchKeys.minus(alreadyLoadedChartsBatchKeys)
if (batchesKeysToLoad.isNotEmpty()) {
actionsFlow.emit(
BatchAction.UpdateBatches(
keys = batchesKeysToLoad,
updateRequest = TokenMarketUpdateRequest.UpdateChart(
interval = TokenMarketListConfig.Interval.H24,
currency = currentAppCurrency().code,
),
async = true,
operationId = batchesKeysToLoad.toString() + "h24",
),
)
}
}
}
fun getTokenMarketById(id: CryptoCurrency.RawID): TokenMarket? {
return batchFlow.state.value.data
.asSequence()
.flatMap { it.data }
.firstOrNull { it.id == id }
}
fun getBatchKeysByItemIds(ids: List<CryptoCurrency.RawID>): Set<Int> {
val currentData = batchFlow.state.value.data
return currentData
.filter { d -> d.data.any { ids.contains(it.id) } }
.map { it.key }
.toSet()
}
private data class ResultBatches(
val uiBatches: List<Batch<Int, List<MarketsListItemUM>>> = emptyList(),
val processedItems: List<Batch<Int, List<TokenMarket>>>? = null,
)
}

View file

@ -1,161 +0,0 @@
package com.tangem.features.onramp.swap.availablepairs.market.converter
import com.tangem.common.ui.charts.state.MarketChartData
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
import com.tangem.common.ui.charts.state.sorted
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated
import com.tangem.core.ui.R
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.compact
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.format.bigdecimal.price
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
import java.math.RoundingMode
internal class SwapMarketsTokenItemConverter(
private val appCurrency: AppCurrency,
) : Converter<TokenMarket, MarketsListItemUM> {
private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(shouldFormatAxis = false)
override fun convert(value: TokenMarket): MarketsListItemUM {
return MarketsListItemUM(
id = value.id,
name = value.name,
currencySymbol = value.symbol,
ratingPosition = value.marketRating?.toString(),
marketCap = value.getMarketCap(),
iconUrl = value.imageUrlLarge,
price = value.getCurrentPrice(),
trendPercentText = value.getTrendPercent(),
trendType = value.getTrendType(),
chartData = value.getChartData(),
isUnder100kMarketCap = value.isUnderMarketCapLimit,
stakingRate = value.yieldRate?.format { percent() }?.let {
resourceReference(R.string.markets_apy_placeholder, wrappedList(it))
},
updateTimestamp = value.updateTimestamp,
networks = value.networks?.map { network ->
MarketsListItemUM.Network(
networkId = network.networkId,
contractAddress = network.contractAddress,
decimalCount = network.decimalCount,
)
},
)
}
fun convertList(items: List<TokenMarket>): List<MarketsListItemUM> = items.map(::convert)
fun update(prev: TokenMarket, prevUI: MarketsListItemUM, new: TokenMarket): MarketsListItemUM {
require(prev.id == new.id) {
"Ids is not the same during update TokenMarket item: previousItem[${prev.id}] != newItem[${new.id}]"
}
return prevUI.copy(
name = new.name,
currencySymbol = new.symbol,
ratingPosition = new.marketRating?.toString(),
marketCap = ifChanged(prev.marketCap, new.marketCap, prevUI.marketCap) { new.getMarketCap() },
iconUrl = new.imageUrlLarge,
price = ifChanged(prev = prev.tokenQuotesShort, new = new.tokenQuotesShort, prevR = prevUI.price) {
new.getCurrentPrice(prev = prev)
},
trendPercentText = ifChanged(
prev.tokenQuotesShort,
new.tokenQuotesShort,
prevUI.trendPercentText,
) { new.getTrendPercent() },
trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() },
chartData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chartData) { new.getChartData() },
)
}
private inline fun <T, R> ifChanged(prev: T, new: T, prevR: R, force: Boolean = false, change: (T) -> R): R {
return if (force || prev != new) change(new) else prevR
}
private fun TokenMarket.getMarketCap(): String? {
val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null
return value.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
).compact(
threeDigitsMethod = true,
)
}
}
private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price {
val prevPrice = prev?.tokenQuotesShort?.currentPrice
val priceText = tokenQuotesShort.currentPrice.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
).price()
}
val changeType = if (prevPrice != null) {
if (tokenQuotesShort.currentPrice > prevPrice) {
PriceChangeType.UP
} else {
PriceChangeType.DOWN
}
} else {
null
}
return MarketsListItemUM.Price(
text = priceText,
annotated = tokenQuotesShort.currentPrice.toMarketsListItemPriceAnnotated(
appCurrencyCode = appCurrency.code,
appCurrencySymbol = appCurrency.symbol,
),
changeType = changeType,
fiatPrice = tokenQuotesShort.currentPrice,
)
}
private fun TokenMarket.getChartData(): MarketChartRawData? {
val chart = tokenCharts.h24
return chart?.let { ct ->
priceAndTimePointValuesConverter.convert(
MarketChartData.Data(
y = ct.priceY.toImmutableList(),
x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
).sorted(),
)
}
}
@Suppress("MagicNumber")
private fun TokenMarket.getTrendType(): PriceChangeType {
val percent = tokenQuotesShort.h24ChangePercent
val scaled = percent?.setScale(4, RoundingMode.HALF_UP)
return when {
scaled == null -> PriceChangeType.NEUTRAL
scaled > BigDecimal.ZERO -> PriceChangeType.UP
scaled < BigDecimal.ZERO -> PriceChangeType.DOWN
else -> PriceChangeType.NEUTRAL
}
}
private fun TokenMarket.getTrendPercent(): String {
val percent = tokenQuotesShort.h24ChangePercent
return percent.format { percent() }
}
}

View file

@ -1,41 +0,0 @@
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 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() {
override val marketsTitle: TextReference = TextReference.Res(R.string.markets_common_title)
override val shouldAssetsCount: Boolean = true
}
}

View file

@ -1,7 +0,0 @@
package com.tangem.features.onramp.swap.availablepairs.model
import com.tangem.core.decompose.navigation.Route
import kotlinx.serialization.Serializable
@Serializable
internal data object AddToPortfolioRoute : Route

View file

@ -1,648 +0,0 @@
package com.tangem.features.onramp.swap.availablepairs.model
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources
import com.tangem.core.analytics.models.event.SwapAnalyticsEvent
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.fields.InputManager
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.lceContent
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
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
import com.tangem.feature.swap.domain.models.domain.SwapPairLeast
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer
import com.tangem.features.onramp.swap.availablepairs.market.SwapMarketsListBatchFlowManager
import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
import com.tangem.features.onramp.swap.entity.AccountCurrencyUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.SetLoadingAccountTokenListTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateAccountTokenListTransformer
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
import com.tangem.features.onramp.utils.ClearSearchBarTransformer
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
import com.tangem.core.ui.R as CoreUiR
private typealias AvailablePairsState = Lce<Throwable, List<SwapPairLeast>>
@Suppress("LongParameterList", "LargeClass")
internal class AvailableSwapPairsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val tokenListUMController: TokenListUMController,
private val searchManager: InputManager,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getAvailablePairsUseCase: GetAvailablePairsUseCase,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
private val excludedBlockchains: ExcludedBlockchains,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
getWalletsUseCase: GetWalletsUseCase,
) : Model() {
val state: StateFlow<TokenListUM> = tokenListUMController.state
private val params: AvailableSwapPairsComponent.Params = paramsContainer.require()
private val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
private val allUserWallets = getWalletsUseCase.invokeSync()
val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute> = SlotNavigation()
val addToPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory
.create(
scope = modelScope,
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value),
settings = AddToPortfolioManager.Settings.ChooseToken,
)
private val accountListFlow = getAccountListUseCaseFlow()
private val availablePairsByNetworkFlow = MutableStateFlow<Map<LeastTokenInfo, AvailablePairsState>>(emptyMap())
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = getSelectedAppCurrencyUseCase.invokeOrDefault()
.stateIn(scope = modelScope, started = SharingStarted.Eagerly, initialValue = AppCurrency.Default)
private val refreshPairsTrigger = MutableSharedFlow<Unit>()
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,
dispatchers = dispatchers,
)
}
private val visibleDefaultMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
init {
subscribeOnUpdateState()
initializeSearchBarCallbacks()
subscribeOnSelectedStatusChange()
subscribeOnAvailablePairsUpdates()
subscribeOnMarketsUpdates()
subscribeOnVisibleMarketItems()
addToPortfolioManager.onDismiss.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.launchIn(modelScope)
addToPortfolioManager.onSuccessAdded.receiveAsFlow()
.onEach { result -> onTokenAddedToPortfolio(result.addedCurrency.currency) }
.launchIn(modelScope)
}
private fun getAccountListUseCaseFlow(): SharedFlow<List<AccountStatus>> {
return singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(params.userWalletId))
.distinctUntilChanged()
.mapNotNull { accountStatusList ->
accountStatusList.accountStatuses.filter {
it is AccountStatus.CryptoPortfolio && it.tokenList !is TokenList.Empty
}
}
.flowOn(dispatchers.default)
.shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1)
}
private fun subscribeOnSelectedStatusChange() {
params.selectedStatus
.filter { it == null }
.onEach { clearSearchState() }
.launchIn(modelScope)
}
private fun initializeSearchBarCallbacks() {
tokenListUMController.update(
transformer = UpdateSearchBarCallbacksTransformer(
onQueryChange = ::onSearchQueryChange,
onActiveChange = ::onSearchBarActiveChange,
),
)
}
private fun subscribeOnUpdateState() {
combine(
flow = getAccountsAndModeFlow(),
flow2 = getAppCurrencyAndBalanceHidingFlow(),
flow3 = params.selectedStatus,
flow4 = searchManager.query,
flow5 = availablePairsByNetworkFlow
.map { it[params.selectedStatus.value?.toLeastTokenInfo()] }
.distinctUntilChanged(),
) { accountListAndMode, appCurrencyAndBalanceHiding, selectedStatus, query, availablePairsState ->
val (accountList, isAccountsMode) = accountListAndMode
availablePairsState?.fold(
ifLoading = {
SetLoadingAccountTokenListTransformer(
appCurrency = appCurrencyAndBalanceHiding.first,
accountList = accountList,
isAccountsMode = isAccountsMode,
)
},
ifContent = { pairs ->
handleContentState(
appCurrencyAndBalanceHiding = appCurrencyAndBalanceHiding,
accountList = accountList,
selectedStatus = selectedStatus,
query = query,
availablePairs = pairs,
isAccountsMode = isAccountsMode,
)
},
ifError = { throwable ->
handleErrorState(
cause = throwable,
networkInfo = params.selectedStatus.value?.toLeastTokenInfo(),
accountList = accountList,
)
},
) ?: SetLoadingAccountTokenListTransformer(
appCurrency = appCurrencyAndBalanceHiding.first,
accountList = accountList,
isAccountsMode = isAccountsMode,
)
}
.onEach(tokenListUMController::update)
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
private fun handleContentState(
appCurrencyAndBalanceHiding: Pair<AppCurrency, Boolean>,
accountList: List<AccountStatus>,
selectedStatus: CryptoCurrencyStatus?,
query: String,
availablePairs: List<SwapPairLeast>,
isAccountsMode: Boolean,
): TokenListUMTransformer {
val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding
val filterByQueryAccountList: Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>> = accountList
.filterCryptoPortfolio()
.associate { accountStatus ->
val statuses = accountStatus.tokenList.flattenCurrencies()
.filterNot { status ->
status.currency.network.rawId == selectedStatus?.currency?.network?.rawId &&
status.currency.id.contractAddress == selectedStatus.currency.id.contractAddress
}
.filterByQuery(query = query)
accountStatus.account to statuses
}
.filterValues { it.isNotEmpty() }
if (availablePairs.isEmpty()) {
return SetNoAvailablePairsTransformer(
appCurrency = appCurrency,
accountList = filterByQueryAccountList,
unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header),
isBalanceHidden = isBalanceHidden,
isAccountsMode = isAccountsMode,
)
}
return if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) {
SetNothingToFoundStateTransformer(
isBalanceHidden = isBalanceHidden,
emptySearchMessageReference = resourceReference(
id = R.string.action_buttons_swap_empty_search_message,
),
)
} else {
UpdateAccountTokenListTransformer(
appCurrency = appCurrency,
onItemClick = ::onPortfolioTokenClick,
accountList = filterByQueryAccountList.filterByAvailability(availablePairs = availablePairs),
isBalanceHidden = isBalanceHidden,
unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header),
isAccountsMode = isAccountsMode,
)
}
}
private fun handleErrorState(
cause: Throwable,
networkInfo: LeastTokenInfo?,
accountList: List<AccountStatus>,
): SetErrorWarningTransformer {
return SetErrorWarningTransformer(
cause = cause,
onRefresh = {
modelScope.launch {
if (networkInfo != null) {
accountList.filterCryptoPortfolio()
.forEach { (_, currencies) ->
updateAvailablePairs(networkInfo, currencies.flattenCurrencies())
}
}
}
},
)
}
private fun subscribeOnAvailablePairsUpdates() {
modelScope.launch {
combine(
params.selectedStatus.filterNotNull(),
refreshPairsTrigger
.onEach { availablePairsByNetworkFlow.value = emptyMap() }
.onStart { emit(Unit) },
) { status, _ -> status }
.collectLatest { selectedStatus ->
val networkInfo = selectedStatus.toLeastTokenInfo()
val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() == true
if (isAlreadyLoaded) return@collectLatest
val accountList = accountListFlow.firstOrNull() ?: return@collectLatest
updateAvailablePairs(
networkInfo = networkInfo,
statuses = accountList.filterCryptoPortfolio()
.flatMap { accountStatus ->
accountStatus.flattenCurrencies()
}.toSet().toList(),
)
}
}
}
private suspend fun updateAvailablePairs(networkInfo: LeastTokenInfo, statuses: List<CryptoCurrencyStatus>) {
runSuspendCatching {
availablePairsByNetworkFlow.update(networkInfo = networkInfo, state = lceLoading())
getAvailablePairsUseCase(
userWallet = userWallet,
initialCurrency = networkInfo,
currencies = statuses.map(CryptoCurrencyStatus::currency),
)
}
.onSuccess { pairs ->
availablePairsByNetworkFlow.update(networkInfo = networkInfo, state = pairs.lceContent())
}
.onFailure { cause ->
availablePairsByNetworkFlow.update(networkInfo = networkInfo, state = cause.lceError())
}
}
private fun MutableStateFlow<Map<LeastTokenInfo, AvailablePairsState>>.update(
networkInfo: LeastTokenInfo,
state: AvailablePairsState,
) {
update { map ->
map.toMutableMap().apply {
this[networkInfo] = state
}
}
}
private fun getAppCurrencyAndBalanceHidingFlow(): Flow<Pair<AppCurrency, Boolean>> {
return combine(
flow = getSelectedAppCurrencyUseCase.invokeOrDefault(),
flow2 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
transform = ::Pair,
)
}
private fun getAccountsAndModeFlow(): Flow<Pair<List<AccountStatus>, Boolean>> {
return combine(
flow = accountListFlow.distinctUntilChanged(),
flow2 = isAccountsModeEnabledUseCase().distinctUntilChanged(),
transform = ::Pair,
)
}
private fun onSearchQueryChange(newQuery: String) {
if (state.value.searchBarUM.query == newQuery) return
modelScope.launch {
tokenListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery))
searchManager.update(newQuery)
searchQueryStateForMarkets.value = newQuery
}
}
private fun onSearchBarActiveChange(isActive: Boolean) {
tokenListUMController.update(
transformer = UpdateSearchBarActiveStateTransformer(
isActive = isActive,
placeHolder = resourceReference(id = R.string.common_search),
),
)
}
private fun List<CryptoCurrencyStatus>.filterByQuery(query: String): List<CryptoCurrencyStatus> {
return filter { status ->
status.currency.name.contains(other = query, ignoreCase = true) ||
status.currency.symbol.contains(other = query, ignoreCase = true)
}
}
private fun Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>>.filterByAvailability(
availablePairs: List<SwapPairLeast>,
): List<AccountAvailabilityUM> {
return map { (account, currencies) ->
AccountAvailabilityUM(
account = account,
currencyList = currencies.map { status ->
val isAvailable = availablePairs.map(SwapPairLeast::to).contains(status.toLeastTokenInfo())
val isAvailableToSwap = isAvailable &&
status.value !is CryptoCurrencyStatus.MissedDerivation &&
status.value !is CryptoCurrencyStatus.Unreachable &&
!status.currency.isCustom
AccountCurrencyUM(
cryptoCurrencyStatus = status,
isAvailable = isAvailableToSwap,
)
},
)
}
}
private fun onPortfolioTokenClick(tokenItem: TokenItemState, status: CryptoCurrencyStatus) {
analyticsEventHandler.send(
SwapAnalyticsEvent.TokenSelected(
token = status.currency.symbol,
source = ScreensSources.Portfolio,
isSearched = state.value.searchBarUM.query.isNotEmpty(),
),
)
clearSearchState()
params.onTokenClick(tokenItem, status)
}
private fun clearSearchState() {
tokenListUMController.update(
transformer = ClearSearchBarTransformer(
placeHolder = resourceReference(id = R.string.common_search),
),
)
modelScope.launch {
searchManager.update("")
}
searchQueryStateForMarkets.value = ""
}
private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo {
return LeastTokenInfo(
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0",
network = currency.network.rawId,
)
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun subscribeOnMarketsUpdates() {
searchQueryStateForMarkets
.map { it.isEmpty() }
.distinctUntilChanged()
.flatMapLatest { isDefaultMode ->
if (isDefaultMode) {
visibleMarketItemIds.value = emptyList()
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 = { 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(
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 onTokenAddedToPortfolio(addedToken: CryptoCurrency) {
modelScope.launch {
bottomSheetNavigation.dismiss()
analyticsEventHandler.send(
SwapAnalyticsEvent.TokenSelected(
token = addedToken.symbol,
source = ScreensSources.Markets,
isSearched = state.value.searchBarUM.query.isNotEmpty(),
),
)
clearSearchState()
// Trigger re-fetch of available pairs (clears cache + re-enters collectLatest)
refreshPairsTrigger.emit(Unit)
// Wait for the added token status to become Loaded
val addedTokenStatus = getAccountCurrencyStatusUseCase(params.userWalletId, addedToken)
.firstOrNull { it.status.value is CryptoCurrencyStatus.Loaded }
?.status
?: return@launch
// Convert to TokenItemState and trigger token selection → navigates to swap
val converter = OnrampTokenItemStateConverterFactory.createAvailableItemConverter(
appCurrency = selectedAppCurrencyFlow.value,
onItemClick = params.onTokenClick,
)
params.onTokenClick(converter.convert(addedTokenStatus), addedTokenStatus)
}
}
private fun addToPortfolioItem(item: MarketsListItemUM) {
val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id)
?: searchMarketsListManager.getTokenMarketById(item.id)
?: return
val param = tokenMarket.toSerializableParam()
val hasOnlyHotWallets = allUserWallets.all { it is UserWallet.Hot }
val networks = tokenMarket.networks?.filter { network ->
BlockchainUtils.isSupportedNetworkId(
networkId = network.networkId,
coinId = tokenMarket.id.value,
contractAddress = network.contractAddress,
excludedBlockchains = excludedBlockchains,
hotExcludedBlockchains = hotWalletExcludedBlockchains,
hasOnlyHotWallets = hasOnlyHotWallets,
)
}?.map { network ->
TokenMarketInfo.Network(
networkId = network.networkId,
isExchangeable = false,
contractAddress = network.contractAddress,
decimalCount = network.decimalCount,
)
}.orEmpty()
addToPortfolioManager.setTokenNetworks(networks)
addToPortfolioManager.setTokenParams(param)
bottomSheetNavigation.activate(AddToPortfolioRoute)
}
private fun subscribeOnVisibleMarketItems() {
modelScope.launch {
visibleMarketItemIds.mapNotNull { rawIds ->
if (rawIds.isNotEmpty()) {
searchMarketsListManager.getBatchKeysByItemIds(rawIds)
} else {
null
}
}.distinctUntilChanged().collectLatest { visibleBatchKeys ->
searchMarketsListManager.loadCharts(visibleBatchKeys)
}
}
modelScope.launch {
visibleDefaultMarketItemIds.mapNotNull { rawIds ->
if (rawIds.isNotEmpty()) {
defaultMarketsListManager.getBatchKeysByItemIds(rawIds)
} else {
null
}
}.distinctUntilChanged().collectLatest { visibleBatchKeys ->
defaultMarketsListManager.loadCharts(visibleBatchKeys)
}
}
}
}

View file

@ -1,114 +0,0 @@
package com.tangem.features.onramp.swap.availablepairs.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import com.tangem.common.ui.markets.MarketsListItem
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
private const val LOADING_PLACEHOLDERS_COUNT = 20
internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) {
item(key = "markets_title") {
val totalCount = (state as? SwapMarketState.Content)?.total
Text(
text = buildAnnotatedString {
append(state.marketsTitle.resolveReference())
if (totalCount != null) {
withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) {
append(" $totalCount")
}
}
},
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(top = TangemTheme.dimens.spacing24, bottom = TangemTheme.dimens.spacing12),
)
}
when (state) {
is SwapMarketState.Loading -> {
items(count = LOADING_PLACEHOLDERS_COUNT, key = { "market_placeholder_$it" }) {
MarketsListItemPlaceholder()
}
}
is SwapMarketState.LoadingError -> {
item(key = "market_loading_error") {
LoadingErrorItem(
modifier = Modifier.fillParentMaxWidth(),
onTryAgain = state.onRetryClicked,
)
}
}
SwapMarketState.SearchNothingFound -> {
item(key = "market_not_found") {
SearchNothingFoundText(
modifier = Modifier.fillParentMaxWidth(),
)
}
}
is SwapMarketState.Content -> {
itemsIndexed(
items = state.items,
key = { _, item -> item.getComposeKey() },
) { index, item ->
MarketsListItem(
model = item,
onClick = { state.onItemClick(item) },
modifier = Modifier.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = state.items.lastIndex,
backgroundColor = TangemTheme.colors.background.action,
),
)
}
}
}
}
@Composable
private fun LoadingErrorItem(onTryAgain: () -> Unit, modifier: Modifier = Modifier) {
Box(
modifier
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
),
contentAlignment = Alignment.Center,
) {
UnableToLoadData(onRetryClick = onTryAgain)
}
}
@Composable
private fun SearchNothingFoundText(modifier: Modifier = Modifier) {
Box(
modifier = modifier.padding(TangemTheme.dimens.spacing16),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResourceSafe(R.string.markets_search_token_no_result_title),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
)
}
}

View file

@ -1,20 +0,0 @@
package com.tangem.features.onramp.swap.di
import com.tangem.features.onramp.component.SwapSelectTokensComponent
import com.tangem.features.onramp.swap.DefaultSwapSelectTokensComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface SwapSelectTokensComponentModule {
@Binds
@Singleton
fun bindSwapSelectTokensComponentFactory(
factory: DefaultSwapSelectTokensComponent.Factory,
): SwapSelectTokensComponent.Factory
}

View file

@ -1,20 +0,0 @@
package com.tangem.features.onramp.swap.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.onramp.swap.model.SwapSelectTokensModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface SwapSelectTokensModelModule {
@Binds
@IntoMap
@ClassKey(SwapSelectTokensModel::class)
fun bindOnrampTokenListModel(model: SwapSelectTokensModel): Model
}

View file

@ -1,63 +0,0 @@
package com.tangem.features.onramp.swap.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.AccountIconUM
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
/**
* Exchange card UI model
*
[REDACTED_AUTHOR]
*/
internal sealed interface ExchangeCardUM {
/** Title reference */
val titleUM: TitleUM
/** Remove button UI model */
val removeButtonUM: RemoveButtonUM?
/**
* Empty state
*
* @property titleUM title reference
* @property subtitleReference empty token subtitle reference
*/
data class Empty(
override val titleUM: TitleUM,
val subtitleReference: TextReference,
) : ExchangeCardUM {
override val removeButtonUM: RemoveButtonUM? = null
}
/**
* Filled
*
* @property titleUM title reference
* @property removeButtonUM remove button UI model
* @property tokenItemState token item state
*/
data class Filled(
override val titleUM: TitleUM,
override val removeButtonUM: RemoveButtonUM?,
val tokenItemState: TokenItemState,
) : ExchangeCardUM
data class RemoveButtonUM(val onClick: () -> Unit)
@Immutable
sealed interface TitleUM {
data class Text(
val title: TextReference,
) : TitleUM
data class Account(
val prefixText: TextReference,
val name: TextReference,
val icon: AccountIconUM.CryptoPortfolio,
) : TitleUM
}
}

View file

@ -1,37 +0,0 @@
package com.tangem.features.onramp.swap.entity
import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeFrom
import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeTo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
/**
* [SwapSelectTokensUM] controller
*
[REDACTED_AUTHOR]
*/
internal class SwapSelectTokensController @Inject constructor() {
val state: StateFlow<SwapSelectTokensUM>
field = MutableStateFlow(
value = SwapSelectTokensUM(
onBackClick = {},
exchangeFrom = createEmptyExchangeFrom(),
exchangeTo = createEmptyExchangeTo(),
isBalanceHidden = false,
),
)
fun update(transform: (SwapSelectTokensUM) -> SwapSelectTokensUM) {
TangemLogger.d("Applying non-name transformation")
state.update(transform)
}
fun update(transformer: SwapSelectTokensUMTransformer) {
TangemLogger.d("Applying ${transformer::class.simpleName ?: "null"}")
state.update(transformer::transform)
}
}

View file

@ -1,17 +0,0 @@
package com.tangem.features.onramp.swap.entity
/**
* Swap select tokens UI model
*
* @property onBackClick callback is called when back button is clicked
* @property exchangeFrom exchange "from" card UI model
* @property exchangeTo exchange "to" card UI model
*
[REDACTED_AUTHOR]
*/
internal data class SwapSelectTokensUM(
val onBackClick: () -> Unit,
val exchangeFrom: ExchangeCardUM,
val exchangeTo: ExchangeCardUM,
val isBalanceHidden: Boolean,
)

View file

@ -1,10 +0,0 @@
package com.tangem.features.onramp.swap.entity
import com.tangem.utils.transformer.Transformer
/**
* Base [SwapSelectTokensUM] transformer
*
[REDACTED_AUTHOR]
*/
internal interface SwapSelectTokensUMTransformer : Transformer<SwapSelectTokensUM>

View file

@ -1,21 +0,0 @@
package com.tangem.features.onramp.swap.entity.transformer
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer
import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeFrom
import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeTo
/**
* Transformer for removing selected "from" token
*
[REDACTED_AUTHOR]
*/
internal object RemoveSelectedFromTokenTransformer : SwapSelectTokensUMTransformer {
override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM {
return prevState.copy(
exchangeFrom = createEmptyExchangeFrom(),
exchangeTo = createEmptyExchangeTo(),
)
}
}

View file

@ -1,29 +0,0 @@
package com.tangem.features.onramp.swap.entity.transformer
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer
import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeTo
/**
* Transformer for removing selected "to" token
*
[REDACTED_AUTHOR]
*/
internal class RemoveSelectedToTokenTransformer(
private val onRemoveFromTokenClick: () -> Unit,
) : SwapSelectTokensUMTransformer {
override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM {
return prevState.copy(
exchangeFrom = prevState.exchangeFrom.showRemoveButton(onClick = onRemoveFromTokenClick),
exchangeTo = createEmptyExchangeTo(),
)
}
private fun ExchangeCardUM.showRemoveButton(onClick: () -> Unit): ExchangeCardUM {
return (this as? ExchangeCardUM.Filled)
?.copy(removeButtonUM = ExchangeCardUM.RemoveButtonUM(onClick = onClick))
?: this
}
}

View file

@ -1,36 +0,0 @@
package com.tangem.features.onramp.swap.entity.transformer
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.domain.models.account.Account
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer
import com.tangem.features.onramp.swap.entity.utils.toFilled
/**
* Transformer for selecting "from" token
*
* @property selectedTokenItemState token item state
* @property onRemoveClick callback is called when remove button is clicked
*
[REDACTED_AUTHOR]
*/
internal class SelectFromTokenTransformer(
private val selectedTokenItemState: TokenItemState,
private val onRemoveClick: () -> Unit,
private val account: Account.CryptoPortfolio?,
private val isAccountsMode: Boolean,
) : SwapSelectTokensUMTransformer {
override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM {
return prevState.copy(
exchangeFrom = prevState.exchangeFrom.toFilled(
selectedTokenItemState = selectedTokenItemState,
removeButtonUM = ExchangeCardUM.RemoveButtonUM(onClick = onRemoveClick),
account = account,
isAccountsMode = isAccountsMode,
isFromCurrency = true,
),
)
}
}

View file

@ -1,38 +0,0 @@
package com.tangem.features.onramp.swap.entity.transformer
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.domain.models.account.Account
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer
import com.tangem.features.onramp.swap.entity.utils.toFilled
/**
* Transformer for selecting "to" token
*
* @property selectedTokenItemState token item state
*
[REDACTED_AUTHOR]
*/
internal class SelectToTokenTransformer(
private val selectedTokenItemState: TokenItemState,
private val isAccountsMode: Boolean,
private val account: Account.CryptoPortfolio?,
) : SwapSelectTokensUMTransformer {
override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM {
return prevState.copy(
exchangeFrom = prevState.exchangeFrom.hideRemoveButton(),
exchangeTo = prevState.exchangeTo.toFilled(
selectedTokenItemState = selectedTokenItemState,
isAccountsMode = isAccountsMode,
account = account,
isFromCurrency = false,
),
)
}
private fun ExchangeCardUM.hideRemoveButton(): ExchangeCardUM {
return (this as? ExchangeCardUM.Filled)?.copy(removeButtonUM = null) ?: this
}
}

View file

@ -1,57 +0,0 @@
package com.tangem.features.onramp.swap.entity.utils
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.account.Account
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
/** Create empty exchange "from" card */
internal fun createEmptyExchangeFrom(): ExchangeCardUM.Empty {
return ExchangeCardUM.Empty(
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)),
subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_swap),
)
}
/** Create empty exchange "to" card */
internal fun createEmptyExchangeTo(): ExchangeCardUM.Empty {
return ExchangeCardUM.Empty(
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_to_title)),
subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_receive),
)
}
/**
* Convert from [ExchangeCardUM] to [ExchangeCardUM.Filled]
*
* @param selectedTokenItemState token item state
* @param removeButtonUM remove button UI model
*/
internal fun ExchangeCardUM.toFilled(
selectedTokenItemState: TokenItemState,
account: Account.CryptoPortfolio?,
isAccountsMode: Boolean,
isFromCurrency: Boolean,
removeButtonUM: ExchangeCardUM.RemoveButtonUM? = null,
): ExchangeCardUM.Filled {
return ExchangeCardUM.Filled(
titleUM = if (account != null && isAccountsMode) {
ExchangeCardUM.TitleUM.Account(
prefixText = if (isFromCurrency) {
resourceReference(R.string.common_from)
} else {
resourceReference(R.string.common_to)
},
name = account.accountName.toUM().value,
icon = CryptoPortfolioIconConverter.convert(account.icon),
)
} else {
titleUM
},
tokenItemState = selectedTokenItemState,
removeButtonUM = removeButtonUM,
)
}

View file

@ -1,187 +0,0 @@
package com.tangem.features.onramp.swap.model
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.onramp.component.SwapSelectTokensComponent
import com.tangem.features.onramp.swap.entity.SwapSelectTokensController
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
import com.tangem.features.onramp.swap.entity.transformer.RemoveSelectedFromTokenTransformer
import com.tangem.features.onramp.swap.entity.transformer.RemoveSelectedToTokenTransformer
import com.tangem.features.onramp.swap.entity.transformer.SelectFromTokenTransformer
import com.tangem.features.onramp.swap.entity.transformer.SelectToTokenTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import javax.inject.Inject
@Suppress("LongParameterList")
internal class SwapSelectTokensModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val controller: SwapSelectTokensController,
private val router: Router,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
) : Model() {
val state: StateFlow<SwapSelectTokensUM> = controller.state
val fromCurrencyStatus: StateFlow<CryptoCurrencyStatus?>
field = MutableStateFlow<CryptoCurrencyStatus?>(value = null)
private val _toCurrencyStatus = MutableStateFlow<CryptoCurrencyStatus?>(value = null)
private val params = paramsContainer.require<SwapSelectTokensComponent.Params>()
private var isAccountsMode: Boolean = false
init {
controller.update { it.copy(onBackClick = ::onBackClick) }
subscribeOnAccountsMode()
subscribeOnBalanceHidingSettings()
}
/**
* Select "from" token
*
* @param selectedTokenItemState selected token item state
* @param status crypto currency status
*/
fun selectFromToken(selectedTokenItemState: TokenItemState, status: CryptoCurrencyStatus) {
analyticsEventHandler.send(
event = MainScreenAnalyticsEvent.SwapTokenClicked(currencySymbol = status.currency.symbol),
)
fromCurrencyStatus.value = status
modelScope.launch {
controller.update(
transformer = SelectFromTokenTransformer(
selectedTokenItemState = selectedTokenItemState,
onRemoveClick = ::onRemoveFromTokenClick,
isAccountsMode = isAccountsMode,
account = getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = params.userWalletId,
currency = status.currency,
).getOrNull()?.account,
),
)
}
}
/**
* Select "to" token
*
* @param selectedTokenItemState selected token item state
* @param status crypto currency status
*/
fun selectToToken(selectedTokenItemState: TokenItemState, status: CryptoCurrencyStatus) {
analyticsEventHandler.send(
event = MainScreenAnalyticsEvent.ReceiveTokenClicked(currencySymbol = status.currency.symbol),
)
modelScope.launch {
_toCurrencyStatus.value = status
controller.update(
transformer = SelectToTokenTransformer(
selectedTokenItemState = selectedTokenItemState,
isAccountsMode = isAccountsMode,
account = getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = params.userWalletId,
currency = status.currency,
).getOrNull()?.account,
),
)
// require some delay to show state with selected "from" and "to" tokens
delay(timeMillis = 500)
router.push(
route = AppRoute.Swap(
fromCryptoCurrency = requireNotNull(fromCurrencyStatus.value).currency,
userWalletId = params.userWalletId,
screenSource = AnalyticsParam.ScreensSources.Main.value,
),
onComplete = {
modelScope.launch {
withTimeout(timeMillis = 500) {
// Return a state with selected only "from" token
removeSelectedToToken()
}
}
},
)
}
}
private fun subscribeOnBalanceHidingSettings() {
getBalanceHidingSettingsUseCase()
.map { it.isBalanceHidden }
.distinctUntilChanged()
.onEach {
controller.update { state -> state.copy(isBalanceHidden = it) }
}
.flowOn(dispatchers.mainImmediate)
.launchIn(modelScope)
}
private fun subscribeOnAccountsMode() {
isAccountsModeEnabledUseCase()
.distinctUntilChanged()
.onEach {
isAccountsMode = it
}
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
private fun onBackClick() {
analyticsEventHandler.send(
event = MainScreenAnalyticsEvent.ButtonClose(source = AnalyticsParam.ScreensSources.Swap),
)
router.pop()
}
private fun onRemoveFromTokenClick() {
val currencySymbol = requireNotNull(fromCurrencyStatus.value?.currency?.symbol) {
"Token was not selected"
}
analyticsEventHandler.send(
event = MainScreenAnalyticsEvent.RemoveTokenClicked(currencySymbol = currencySymbol),
)
removeSelectedFromToken()
}
private fun removeSelectedFromToken() {
fromCurrencyStatus.value = null
controller.update(transformer = RemoveSelectedFromTokenTransformer)
}
private fun removeSelectedToToken() {
_toCurrencyStatus.value = null
controller.update(
transformer = RemoveSelectedToTokenTransformer(onRemoveFromTokenClick = ::removeSelectedFromToken),
)
}
}

View file

@ -1,214 +0,0 @@
package com.tangem.features.onramp.swap.ui
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountLabel
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.rows.NetworkTitle
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SwapSelectTokenScreenTestTags
import com.tangem.core.ui.utils.dashedBorder
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
/**
* Exchange card
*
* @param state state
* @param isBalanceHidden is balance hidden
* @param modifier modifier
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun ExchangeCard(state: ExchangeCardUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.heightIn(min = 116.dp)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.primary)
.testTag(SwapSelectTokenScreenTestTags.YOU_SWAP_BLOCK),
verticalArrangement = Arrangement.SpaceBetween,
) {
Title(
titleUM = state.titleUM,
removeButtonUM = state.removeButtonUM,
)
AnimatedContent(
targetState = state,
transitionSpec = {
fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90))
.togetherWith(fadeOut(animationSpec = tween(durationMillis = 90)))
},
label = "TokenItem's changing",
) { animatedState ->
when (animatedState) {
is ExchangeCardUM.Empty -> EmptyTokenBlock(text = animatedState.subtitleReference)
is ExchangeCardUM.Filled -> {
TokenItem(state = animatedState.tokenItemState, isBalanceHidden = isBalanceHidden)
}
}
}
}
}
@Composable
private fun Title(titleUM: ExchangeCardUM.TitleUM, removeButtonUM: ExchangeCardUM.RemoveButtonUM?) {
NetworkTitle(
title = {
AnimatedContent(
titleUM,
) { currentState ->
when (currentState) {
is ExchangeCardUM.TitleUM.Account -> Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = currentState.prefixText.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
AccountLabel(
name = currentState.name,
icon = currentState.icon,
iconSize = AccountIconSize.ExtraSmall,
nameStyle = TangemTheme.typography.subtitle2,
nameColor = TangemTheme.colors.text.tertiary,
)
}
is ExchangeCardUM.TitleUM.Text -> Text(
text = currentState.title.resolveReference(),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.subtitle2,
)
}
}
},
action = { RemoveButton(state = removeButtonUM) },
)
}
@Composable
private fun RemoveButton(state: ExchangeCardUM.RemoveButtonUM?) {
AnimatedVisibility(visible = state != null) {
state ?: return@AnimatedVisibility
Text(
text = stringResourceSafe(id = R.string.manage_tokens_remove),
modifier = Modifier.clickable(
indication = ripple(bounded = false),
interactionSource = remember { MutableInteractionSource() },
onClick = state.onClick,
),
color = TangemTheme.colors.text.accent,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.body2,
)
}
}
@Composable
private fun EmptyTokenBlock(text: TextReference, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.padding(horizontal = 12.dp, vertical = 13.dp)
.heightIn(min = 50.dp)
.fillMaxWidth()
.dashedBorder(
color = TangemTheme.colors.icon.informative,
shape = RoundedCornerShape(16.dp),
dashLength = 2.dp,
gapLength = 6.dp,
)
.padding(vertical = 15.dp),
contentAlignment = Alignment.Center,
) {
Text(
text = text.resolveReference(),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.body2,
modifier = Modifier.testTag(SwapSelectTokenScreenTestTags.CHOOSE_TOKEN_TEXT),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ExchangeCard(@PreviewParameter(ExchangeCardUMProvider::class) state: ExchangeCardUM) {
TangemThemePreview {
ExchangeCard(
state = state,
isBalanceHidden = false,
modifier = Modifier
.background(TangemTheme.colors.background.secondary)
.padding(16.dp),
)
}
}
private class ExchangeCardUMProvider : PreviewParameterProvider<ExchangeCardUM> {
override val values: Sequence<ExchangeCardUM> = sequenceOf(
ExchangeCardUM.Empty(
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)),
subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_swap),
),
createFilled(removeButtonUM = null),
createFilled(removeButtonUM = ExchangeCardUM.RemoveButtonUM { }),
)
private fun createFilled(removeButtonUM: ExchangeCardUM.RemoveButtonUM?): ExchangeCardUM.Filled {
return ExchangeCardUM.Filled(
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)),
removeButtonUM = removeButtonUM,
tokenItemState = TokenItemState.Content(
id = "1",
iconState = CurrencyIconState.Locked,
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"),
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
price = "34 496,75 \$",
priceChangePercent = "0,43 %",
type = PriceChangeType.DOWN,
),
onItemClick = {},
onItemLongClick = {},
),
)
}
}

View file

@ -1,214 +0,0 @@
package com.tangem.features.onramp.swap.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
private const val LOAD_MORE_BUFFER = 25
/**
* Swap select tokens
*
* @param state state
* @param selectFromTokenListComponent select "from" token list component
* @param selectToTokenListComponent select "to" token list component
* @param modifier modifier
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun SwapSelectTokens(
state: SwapSelectTokensUM,
selectFromTokenListComponent: OnrampTokenListComponent,
selectFromTokenListState: TokenListUM,
selectToTokenListComponent: AvailableSwapPairsComponent,
selectToTokenListState: TokenListUM,
modifier: Modifier = Modifier,
) {
BackHandler(onBack = state.onBackClick)
val nestedScrollConnection = rememberHideKeyboardNestedScrollConnection()
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = modifier
.nestedScroll(nestedScrollConnection)
.background(TangemTheme.colors.background.secondary)
.imePadding()
.systemBarsPadding(),
state = lazyListState,
contentPadding = PaddingValues(bottom = 8.dp),
) {
swapSelectTokensContent(
state = state,
selectFromTokenListComponent = selectFromTokenListComponent,
selectFromTokenListState = selectFromTokenListState,
selectToTokenListComponent = selectToTokenListComponent,
selectToTokenListState = selectToTokenListState,
)
}
ScrollToTopEffect(state = state, lazyListState = lazyListState)
MarketsHandlers(
state = state,
selectToTokenListState = selectToTokenListState,
lazyListState = lazyListState,
)
}
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.swapSelectTokensContent(
state: SwapSelectTokensUM,
selectFromTokenListComponent: OnrampTokenListComponent,
selectFromTokenListState: TokenListUM,
selectToTokenListComponent: AvailableSwapPairsComponent,
selectToTokenListState: TokenListUM,
) {
stickyHeader(key = "header") {
AppBarWithBackButton(
onBackClick = state.onBackClick,
text = stringResourceSafe(id = R.string.common_swap),
iconRes = R.drawable.ic_close_24,
containerColor = TangemTheme.colors.background.secondary,
)
}
item(key = "exchange_from", contentType = "exchange_from") {
ExchangeCard(
state = state.exchangeFrom,
isBalanceHidden = state.isBalanceHidden,
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(top = 8.dp, bottom = 12.dp)
.animateItem(),
)
}
if (state.exchangeFrom is ExchangeCardUM.Empty) {
with(selectFromTokenListComponent) {
content(uiState = selectFromTokenListState, modifier = Modifier)
}
}
if (state.exchangeFrom is ExchangeCardUM.Filled) {
exchangeToSection(
state = state,
selectToTokenListComponent = selectToTokenListComponent,
selectToTokenListState = selectToTokenListState,
)
}
}
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.exchangeToSection(
state: SwapSelectTokensUM,
selectToTokenListComponent: AvailableSwapPairsComponent,
selectToTokenListState: TokenListUM,
) {
item(key = "exchange_to", contentType = "exchange_to") {
if (selectToTokenListState.warning != NotificationUM.Warning.SwapNoAvailablePair) {
ExchangeCard(
state = state.exchangeTo,
isBalanceHidden = state.isBalanceHidden,
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 12.dp)
.animateItem(),
)
}
}
if (state.exchangeTo is ExchangeCardUM.Empty) {
with(selectToTokenListComponent) {
content(uiState = selectToTokenListState, modifier = Modifier.padding(horizontal = 16.dp))
}
}
}
@Composable
private fun ScrollToTopEffect(state: SwapSelectTokensUM, lazyListState: LazyListState) {
LaunchedEffect(state.exchangeFrom !is ExchangeCardUM.Empty) {
lazyListState.scrollToItem(index = 0)
}
}
@Composable
private fun MarketsHandlers(
state: SwapSelectTokensUM,
selectToTokenListState: TokenListUM,
lazyListState: LazyListState,
) {
// Markets for "to" token list
if (state.exchangeFrom is ExchangeCardUM.Filled && state.exchangeTo is ExchangeCardUM.Empty) {
MarketsPaginationHandler(
marketsState = selectToTokenListState.marketsState,
lazyListState = lazyListState,
)
}
}
@Composable
private fun MarketsPaginationHandler(marketsState: SwapMarketState?, lazyListState: LazyListState) {
(marketsState as? SwapMarketState.Content)?.let { content ->
VisibleItemsTracker(lazyListState = lazyListState, marketState = content)
InfiniteListHandler(
listState = lazyListState,
buffer = LOAD_MORE_BUFFER,
triggerLoadMoreCheckOnItemsCountChange = true,
onLoadMore = remember(content) {
{
content.loadMore()
true
}
},
)
}
}
@Composable
private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapMarketState.Content) {
val visibleItems by remember {
derivedStateOf {
lazyListState.layoutInfo.visibleItemsInfo
.mapNotNull { itemInfo ->
marketState.items.find { it.getComposeKey() == itemInfo.key }?.id
}
}
}
LaunchedEffect(visibleItems) {
marketState.visibleIdsChanged(visibleItems)
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.onramp.swap.entity
package com.tangem.features.onramp.tokenlist.entity
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus

View file

@ -3,7 +3,6 @@ package com.tangem.features.onramp.tokenlist.entity
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState
import kotlinx.collections.immutable.ImmutableList
/**
@ -13,7 +12,6 @@ import kotlinx.collections.immutable.ImmutableList
* @property availableItems available items (search bar, header, tokens)
* @property unavailableItems unavailable items (header, tokens)
* @property isBalanceHidden flag that indicates if balance should be hidden
* @property marketsState markets list state (null when markets should not be shown)
*
[REDACTED_AUTHOR]
*/
@ -23,8 +21,7 @@ internal data class TokenListUM(
val unavailableItems: ImmutableList<TokensListItemUM>,
val tokensListData: TokenListUMData,
val isBalanceHidden: Boolean,
val warning: NotificationUM? = null,
val marketsState: SwapMarketState? = null,
val warning: NotificationUM? = null
)
internal sealed interface TokenListUMData {

View file

@ -1,4 +1,4 @@
package com.tangem.features.onramp.swap.availablepairs.entity.converters
package com.tangem.features.onramp.tokenlist.entity.transformer
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
import com.tangem.common.ui.account.TokensListPortfolioItemConverter

View file

@ -1,4 +1,4 @@
package com.tangem.features.onramp.swap.availablepairs.entity.converters
package com.tangem.features.onramp.tokenlist.entity.transformer
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.state.TokenItemState

View file

@ -4,8 +4,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingAccountTokenItemConverter
import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer

View file

@ -8,7 +8,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
import com.tangem.features.onramp.tokenlist.entity.AccountAvailabilityUM
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toPersistentList

View file

@ -8,7 +8,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
import com.tangem.features.onramp.tokenlist.entity.AccountAvailabilityUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer

View file

@ -26,8 +26,8 @@ import com.tangem.domain.tokens.GetAssetRequirementsUseCase
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
import com.tangem.features.onramp.swap.entity.AccountCurrencyUM
import com.tangem.features.onramp.tokenlist.entity.AccountAvailabilityUM
import com.tangem.features.onramp.tokenlist.entity.AccountCurrencyUM
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
import com.tangem.features.onramp.tokenlist.entity.*
import com.tangem.features.onramp.tokenlist.entity.transformer.SetLoadingAccountTokenListTransformer

View file

@ -21,7 +21,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
import com.tangem.core.ui.components.fields.entity.SearchBarUM
@ -37,26 +36,11 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.BuyTokenScreenTestTags
import com.tangem.core.ui.utils.lazyListItemPosition
import com.tangem.features.onramp.swap.availablepairs.ui.swapMarketsListItems
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider
import kotlinx.collections.immutable.ImmutableList
/**
* Token list for swap - automatically switches between normal and search mode with markets
*
* @param state state
*
*/
internal fun LazyListScope.onrampSwapTokenList(state: TokenListUM) {
if (state.marketsState != null) {
onrampTokenListWithMarkets(state = state)
} else {
onrampTokenList(state = state)
}
}
/**
* Token list - normal mode (without markets)
*
@ -74,39 +58,6 @@ internal fun LazyListScope.onrampTokenList(state: TokenListUM) {
tokensListData(state = state)
}
/**
* Token list with markets - search mode
*
* @param state state
*/
private fun LazyListScope.onrampTokenListWithMarkets(state: TokenListUM) {
val itemModifier = Modifier.padding(horizontal = 16.dp)
warningOrSearchBar(state = state, itemModifier = itemModifier)
// Check if user has any assets to show
val hasAssets = state.availableItems.isNotEmpty() ||
state.unavailableItems.isNotEmpty() ||
state.tokensListData.totalTokensCount != 0
if (hasAssets) {
assetsTitle(
count = state.tokensListData.totalTokensCount,
showCount = state.marketsState?.shouldAssetsCount == true,
)
tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden)
tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden)
tokensListData(state = state)
item { SpacerH32() }
}
state.marketsState?.let(::swapMarketsListItems)
}
private fun LazyListScope.warningOrSearchBar(state: TokenListUM, itemModifier: Modifier) {
if (state.warning == null) {
searchBarItem(searchBarUM = state.searchBarUM, modifier = itemModifier)

View file

@ -49,11 +49,9 @@ import com.tangem.datasource.api.express.models.request.LeastTokenInfo as Networ
internal class DefaultSwapRepository(
private val tangemExpressApi: TangemExpressApi,
private val coroutineDispatcher: CoroutineDispatcherProvider,
private val walletManagersFacade: WalletManagersFacade,
private val errorsDataConverter: ErrorsDataConverter,
private val dataSignatureVerifier: DataSignatureVerifier,
private val appPreferencesStore: AppPreferencesStore,
private val rampStateManager: RampStateManager,
private val expressHistoryDao: ExpressHistoryDao,
moshi: Moshi,
) : SwapRepository {
@ -122,98 +120,6 @@ internal class DefaultSwapRepository(
}
}
override suspend fun getPairsOnly(
userWallet: UserWallet,
initialCurrency: LeastTokenInfo,
currencyList: List<CryptoCurrency>,
isIgnoreExpress: Boolean,
): PairsWithProviders {
return withContext(coroutineDispatcher.io) {
val currenciesList = filterByAssetRequirements(userWallet, currencyList)
if (isIgnoreExpress) {
buildLocalPairs(initialCurrency, currenciesList)
} else {
fetchExpressPairs(userWallet, initialCurrency, currenciesList)
}
}
}
private fun buildLocalPairs(
initialCurrency: LeastTokenInfo,
currenciesList: List<NetworkLeastTokenInfo>,
): PairsWithProviders {
val pairs = currenciesList.map { tokenInfo ->
SwapPairLeast(
from = initialCurrency,
to = LeastTokenInfo(
contractAddress = tokenInfo.contractAddress,
network = tokenInfo.network,
),
providers = emptyList(),
)
}
return PairsWithProviders(pairs = pairs, allProviders = emptyList())
}
private suspend fun fetchExpressPairs(
userWallet: UserWallet,
initialCurrency: LeastTokenInfo,
currenciesList: List<NetworkLeastTokenInfo>,
): PairsWithProviders {
try {
val initial = NetworkLeastTokenInfo(
contractAddress = initialCurrency.contractAddress,
network = initialCurrency.network,
)
val allPairs = supervisorScope {
val pairsDeferred = async {
getPairsInternal(
userWallet = userWallet,
from = arrayListOf(initial),
to = currenciesList,
)
}
val reversedPairsDeferred = async {
getPairsInternal(
userWallet = userWallet,
from = currenciesList,
to = arrayListOf(initial),
)
}
pairsDeferred.await().getOrThrow() + reversedPairsDeferred.await().getOrThrow()
}
return swapPairInfoConverter.convert(
SwapPairsWithProviders(
swapPair = allPairs,
providers = emptyList(),
),
)
} catch (exception: Exception) {
if (exception is ApiResponseError.HttpException) {
throw ExpressException(errorsDataConverter.convert(exception.errorBody.orEmpty()))
} else {
throw exception
}
}
}
private suspend fun filterByAssetRequirements(
userWallet: UserWallet,
currencyList: List<CryptoCurrency>,
): List<NetworkLeastTokenInfo> {
return currencyList
.filter { currency ->
val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, currency)
rampStateManager.checkAssetRequirements(requirements)
}
.map { currency -> leastTokenInfoConverter.convert(currency) }
}
private suspend fun getPairsInternal(
userWallet: UserWallet,
from: List<NetworkLeastTokenInfo>,

View file

@ -39,22 +39,18 @@ internal class SwapDataModule {
tangemExpressApi: TangemExpressApi,
coroutineDispatcher: CoroutineDispatcherProvider,
dataSignature: DataSignatureVerifier,
walletManagerFacade: WalletManagersFacade,
errorsDataConverter: ErrorsDataConverter,
@NetworkMoshi moshi: Moshi,
appPreferencesStore: AppPreferencesStore,
rampStateManager: RampStateManager,
expressHistoryDao: ExpressHistoryDao,
): SwapRepository {
return DefaultSwapRepository(
tangemExpressApi = tangemExpressApi,
coroutineDispatcher = coroutineDispatcher,
walletManagersFacade = walletManagerFacade,
errorsDataConverter = errorsDataConverter,
dataSignatureVerifier = dataSignature,
moshi = moshi,
appPreferencesStore = appPreferencesStore,
rampStateManager = rampStateManager,
expressHistoryDao = expressHistoryDao,
)
}

View file

@ -1,25 +0,0 @@
package com.tangem.feature.swap.domain
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
import com.tangem.feature.swap.domain.models.domain.SwapPairLeast
class GetAvailablePairsUseCase(
private val swapRepository: SwapRepository,
) {
suspend operator fun invoke(
userWallet: UserWallet,
initialCurrency: LeastTokenInfo,
currencies: List<CryptoCurrency>,
): List<SwapPairLeast> {
return swapRepository.getPairsOnly(
userWallet = userWallet,
initialCurrency = initialCurrency,
currencyList = currencies,
isIgnoreExpress = true,
).pairs
}
}

View file

@ -16,14 +16,6 @@ interface SwapRepository {
currencyList: List<CryptoCurrency>,
): PairsWithProviders
/** Express getPairs request variant without providers request */
suspend fun getPairsOnly(
userWallet: UserWallet,
initialCurrency: LeastTokenInfo,
currencyList: List<CryptoCurrency>,
isIgnoreExpress: Boolean = false,
): PairsWithProviders
suspend fun getExchangeStatus(
userWallet: UserWallet?,
userWalletId: UserWalletId,