Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-09 13:53:15 +04:00
parent e2b5e5b07b
commit 81b2f9e4a3
10 changed files with 375 additions and 0 deletions

View file

@ -0,0 +1,23 @@
package com.tangem.tap.di.domain
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
import com.tangem.feature.swap.domain.api.SwapRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
[REDACTED_AUTHOR]
*/
@Module
@InstallIn(SingletonComponent::class)
internal object SwapDomainModule {
@Provides
@Singleton
fun provideGetAvailablePairsUseCase(swapRepository: SwapRepository): GetAvailablePairsUseCase {
return GetAvailablePairsUseCase(swapRepository = swapRepository)
}
}

View file

@ -14,6 +14,9 @@ android {
dependencies {
/** Project - API */
implementation(projects.features.onramp.api)
implementation(projects.features.swap.domain)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
/** Project - Core */
implementation(projects.core.decompose)

View file

@ -0,0 +1,30 @@
package com.tangem.features.onramp.swap.availablepairs
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.StateFlow
/** Token list component that present list of available tokens for swap */
@Stable
internal interface AvailableSwapPairsComponent : ComposableContentComponent {
/** 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

@ -0,0 +1,38 @@
package com.tangem.features.onramp.swap.availablepairs
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.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel
import com.tangem.features.onramp.tokenlist.ui.TokenList
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Stable
internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: AvailableSwapPairsComponent.Params,
) : AvailableSwapPairsComponent, AppComponentContext by context {
private val model: AvailableSwapPairsModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
TokenList(state = state, modifier = modifier)
}
@AssistedFactory
interface Factory : AvailableSwapPairsComponent.Factory {
override fun create(
context: AppComponentContext,
params: AvailableSwapPairsComponent.Params,
): DefaultAvailableSwapPairsComponent
}
}

View file

@ -0,0 +1,20 @@
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

@ -0,0 +1,20 @@
package com.tangem.features.onramp.swap.availablepairs.di
import com.tangem.core.decompose.di.DecomposeComponent
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(DecomposeComponent::class)
internal interface AvailableSwapPairsModelModule {
@Binds
@IntoMap
@ClassKey(AvailableSwapPairsModel::class)
fun bindAvailableSwapPairsModel(model: AvailableSwapPairsModel): Model
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.onramp.swap.availablepairs.entity.converters
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.utils.converter.Converter
/**
* Convert from [CryptoCurrencyStatus] to loading [TokensListItemUM.Token] state
*
[REDACTED_AUTHOR]
*/
internal object LoadingTokenListItemConverter : Converter<CryptoCurrencyStatus, TokensListItemUM.Token> {
override fun convert(value: CryptoCurrencyStatus): TokensListItemUM.Token {
return TokensListItemUM.Token(
state = TokenItemState.Loading(
id = value.currency.id.value,
iconState = CurrencyIconState.Loading,
titleState = TokenItemState.TitleState.Loading,
subtitleState = TokenItemState.SubtitleState.Loading,
),
)
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.onramp.swap.availablepairs.entity.transformers
import com.tangem.domain.tokens.model.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.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).toImmutableList(),
)
}
}

View file

@ -0,0 +1,174 @@
package com.tangem.features.onramp.swap.availablepairs.model
import arrow.core.getOrElse
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenList
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.onramp.impl.R
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoadingTokenItemsTransformer
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer
import com.tangem.features.onramp.utils.SearchManager
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
internal class AvailableSwapPairsModel @Inject constructor(
paramsContainer: ParamsContainer,
getTokenListUseCase: GetTokenListUseCase,
override val dispatchers: CoroutineDispatcherProvider,
private val tokenListUMController: TokenListUMController,
private val searchManager: SearchManager,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getAvailablePairsUseCase: GetAvailablePairsUseCase,
) : Model() {
val state: StateFlow<TokenListUM> = tokenListUMController.state
private var params: AvailableSwapPairsComponent.Params = paramsContainer.require()
private val tokenListFlow = getTokenListUseCase.launch(userWalletId = params.userWalletId)
.distinctUntilChanged()
.map { maybeTokenList ->
maybeTokenList.getOrElse(
ifLoading = { it ?: TokenList.Empty },
ifError = { TokenList.Empty },
)
.flattenCurrencies()
}
.shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1)
private val availablePairsByNetworkFlow = MutableStateFlow<Map<LeastTokenInfo, List<SwapPairLeast>>>(emptyMap())
init {
subscribeOnUpdateState()
subscribeOnAvailablePairsUpdates()
}
private fun subscribeOnUpdateState() {
combine(
flow = tokenListFlow,
flow2 = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(),
flow3 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(),
flow4 = searchManager.query,
flow5 = availablePairsByNetworkFlow
.map { it[params.selectedStatus.value?.toLeastTokenInfo()].orEmpty() }
.distinctUntilChanged(),
) { currencies, appCurrency, isBalanceHidden, query, availablePairs ->
if (availablePairs.isEmpty()) {
SetLoadingTokenItemsTransformer(currencies)
} else {
val filterTokenList = currencies
.filterByQuery(query = query)
.filterByAvailability(availablePairs = availablePairs)
Timber.e("${filterTokenList[true].orEmpty().size}")
UpdateTokenItemsTransformer(
appCurrency = appCurrency,
onItemClick = params.onTokenClick,
statuses = filterTokenList,
isBalanceHidden = isBalanceHidden,
hasSearchBar = currencies.isNotEmpty(),
onQueryChange = ::onSearchQueryChange,
onActiveChange = ::onSearchBarActiveChange,
)
}
}
.onEach(tokenListUMController::update)
.flowOn(dispatchers.main)
.launchIn(modelScope)
}
private fun subscribeOnAvailablePairsUpdates() {
modelScope.launch {
params.selectedStatus
.filterNotNull()
.collectLatest { selectedStatus ->
val initialCurrency = selectedStatus.toLeastTokenInfo()
val tokenList = tokenListFlow.firstOrNull() ?: return@collectLatest
val availablePairs = availablePairsByNetworkFlow.value[initialCurrency]
if (!availablePairs.isNullOrEmpty()) return@collectLatest
val pairs = getAvailablePairsUseCase(
initialCurrency = initialCurrency,
currencies = tokenList.map(CryptoCurrencyStatus::currency),
)
Timber.e("$initialCurrency = ${pairs.size}")
availablePairsByNetworkFlow.update {
it.toMutableMap().apply {
put(initialCurrency, pairs)
}
}
}
}
}
private fun onSearchQueryChange(newQuery: String) {
val searchBar = tokenListUMController.getSearchBar()
if (searchBar?.searchBarUM?.query == newQuery) return
modelScope.launch {
tokenListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery))
searchManager.update(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 {
it.currency.name.contains(other = query, ignoreCase = true) ||
it.currency.symbol.contains(other = query, ignoreCase = true)
}
}
private fun List<CryptoCurrencyStatus>.filterByAvailability(
availablePairs: List<SwapPairLeast>,
): Map<Boolean, List<CryptoCurrencyStatus>> {
return groupBy { status ->
val isAvailable = availablePairs.map(SwapPairLeast::to).contains(status.toLeastTokenInfo())
isAvailable &&
status.value !is CryptoCurrencyStatus.MissedDerivation &&
status.value !is CryptoCurrencyStatus.Unreachable
}
}
private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo {
return LeastTokenInfo(
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0",
network = currency.network.backendId,
)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.feature.swap.domain
import com.tangem.domain.tokens.model.CryptoCurrency
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(
initialCurrency: LeastTokenInfo,
currencies: List<CryptoCurrency>,
): List<SwapPairLeast> {
return swapRepository.getPairsOnly(initialCurrency, currencies).pairs
}
}