Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-03 14:49:04 +04:00
parent 03691635a2
commit e2b489f0b9
20 changed files with 144 additions and 637 deletions

View file

@ -31,7 +31,6 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.onramp.hottokens.model.HotCryptoModel
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddToPortfolioComponent
@ -48,38 +47,29 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: HotCryptoComponent.Params,
private val onrampAddToPortfolioComponentFactory: OnrampAddToPortfolioComponent.Factory,
private val accountsFeatureToggles: AccountsFeatureToggles,
portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
addTokenComponentFactory: OnrampAddTokenComponent.Factory,
) : HotCryptoComponent, AppComponentContext by context {
private val model: HotCryptoModel = getOrCreateModel(params)
private val portfolioSelectorComponent: PortfolioSelectorComponent? by lazy {
if (accountsFeatureToggles.isFeatureEnabled) {
portfolioSelectorComponentFactory.create(
context = child("portfolioSelectorComponent"),
params = PortfolioSelectorComponent.Params(
portfolioFetcher = requireNotNull(model.portfolioFetcher),
controller = model.portfolioSelectorController,
),
)
} else {
null
}
private val portfolioSelectorComponent: PortfolioSelectorComponent by lazy {
portfolioSelectorComponentFactory.create(
context = child("portfolioSelectorComponent"),
params = PortfolioSelectorComponent.Params(
portfolioFetcher = model.portfolioFetcher,
controller = model.portfolioSelectorController,
),
)
}
private val addTokenComponent: OnrampAddTokenComponent? by lazy {
if (accountsFeatureToggles.isFeatureEnabled) {
addTokenComponentFactory.create(
context = child("addTokenComponent"),
params = OnrampAddTokenComponent.Params(
callbacks = model,
tokenToAdd = model.hotCryptoToAddDataFlow,
),
)
} else {
null
}
private val addTokenComponent: OnrampAddTokenComponent by lazy {
addTokenComponentFactory.create(
context = child("addTokenComponent"),
params = OnrampAddTokenComponent.Params(
callbacks = model,
tokenToAdd = model.hotCryptoToAddDataFlow,
),
)
}
private val bottomSheetSlot = childSlot(
@ -106,9 +96,7 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor(
HotCrypto(state, modifier)
bottomSheet.child?.instance?.BottomSheet()
if (accountsFeatureToggles.isFeatureEnabled) {
AddHotCryptoBottomSheet()
}
AddHotCryptoBottomSheet()
}
@Composable
@ -133,7 +121,7 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor(
content = TangemBottomSheetConfigContent.Empty,
),
containerColor = TangemTheme.colors.background.tertiary,
title = { state ->
title = { _ ->
AnimatedContent(targetState = contentStack.value) { stack ->
BottomSheetTitle(
stack = stack,
@ -142,7 +130,7 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor(
)
}
},
content = { state ->
content = { _ ->
AnimatedContent(targetState = contentStack.value) { stack ->
val paddingModifier = Modifier.padding(
start = 16.dp,
@ -221,8 +209,8 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor(
}
private fun contentChild(config: OnrampAddTokenRoute): ComposableContentComponent = when (config) {
OnrampAddTokenRoute.AddToken -> requireNotNull(addTokenComponent)
OnrampAddTokenRoute.PortfolioSelector -> requireNotNull(portfolioSelectorComponent)
OnrampAddTokenRoute.AddToken -> addTokenComponent
OnrampAddTokenRoute.PortfolioSelector -> portfolioSelectorComponent
OnrampAddTokenRoute.Empty -> ComposableContentComponent.EMPTY
}

View file

@ -1,9 +1,6 @@
package com.tangem.features.onramp.hottokens.model
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.popToFirst
import com.arkivanov.decompose.router.stack.pushNew
@ -13,7 +10,6 @@ import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.data.common.currency.getCoinId
@ -21,17 +17,11 @@ import com.tangem.data.common.currency.getTokenId
import com.tangem.data.common.currency.isCustomCoin
import com.tangem.data.common.currency.isCustomToken
import com.tangem.data.common.network.NetworkFactory
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.GetHotCryptoUseCase
import com.tangem.domain.onramp.model.HotCryptoCurrency
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorController
import com.tangem.features.onramp.hottokens.HotCryptoComponent
@ -51,7 +41,6 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -59,9 +48,6 @@ import javax.inject.Inject
* Hot crypto model
*
* @param paramsContainer params container
* @param getHotCryptoUseCase use case for getting hot crypto
* @param getSelectedAppCurrencyUseCase use case for getting selected app currency
* @property getCryptoCurrencyStatusSyncUseCase use case for getting crypto currency status by id
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
@ -70,22 +56,17 @@ import javax.inject.Inject
@ModelScoped
internal class HotCryptoModel @Inject constructor(
paramsContainer: ParamsContainer,
private val getHotCryptoUseCase: GetHotCryptoUseCase,
private val callbackDelegate: HotCryptoModelCallbackDelegate,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
override val dispatchers: CoroutineDispatcherProvider,
private val hotCryptoPortfolioDataLoader: HotCryptoPortfolioDataLoader,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
val portfolioSelectorController: PortfolioSelectorController,
private val networkFactory: NetworkFactory,
private val portfolioFetcherFactory: PortfolioFetcher.Factory,
portfolioFetcherFactory: PortfolioFetcher.Factory,
) : Model(), OnrampAddTokenComponent.Callbacks by callbackDelegate {
val bottomSheetNavigation: SlotNavigation<OnrampAddToPortfolioBSConfig> = SlotNavigation()
val portfolioFetcher: PortfolioFetcher?
val portfolioFetcher: PortfolioFetcher
val bottomSheetNavigationV2 = StackNavigation<OnrampAddTokenRoute>()
private val addHotCryptoJob = JobHolder()
val hotCryptoToAddDataFlow: MutableSharedFlow<AddHotCryptoData> = MutableSharedFlow(
@ -99,53 +80,30 @@ internal class HotCryptoModel @Inject constructor(
private val params: HotCryptoComponent.Params = paramsContainer.require()
init {
if (accountsFeatureToggles.isFeatureEnabled) {
portfolioFetcher = portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.Wallet(params.userWalletId),
scope = modelScope,
)
combineData()
} else {
portfolioFetcher = null
combineDataOld()
}
portfolioFetcher = portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.Wallet(params.userWalletId),
scope = modelScope,
)
combineData()
}
private fun combineData() {
combine(
flow = hotCryptoPortfolioDataLoader.loadPortfolioData(params.userWalletId),
flow2 = isAccountsModeEnabledUseCase.invoke(),
transform = { data, isAccountMode ->
hotCryptoPortfolioDataLoader.loadPortfolioData(params.userWalletId)
.map { data ->
HotTokenItemStateConverter(
appCurrency = data.appCurrency,
onItemClick = { tokenItemState, hotCryptoCurrency ->
onItemClick = { _, hotCryptoCurrency ->
startAddTokenFlow(currency = hotCryptoCurrency, hotCryptoPortfolioData = data)
},
)
.convertList(data.allHotCrypto)
.map(TokensListItemUM::Token)
},
)
}
.onEach { items -> state.update { HotCryptoUM(items = it.buildItems(items)) } }
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
private fun combineDataOld() {
combine(
flow = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } },
flow2 = getHotCryptoUseCase(params.userWalletId),
) { appCurrency, currencies ->
HotTokenItemStateConverter(appCurrency = appCurrency, onItemClick = ::onTokenClick)
.convertList(currencies)
.map(TokensListItemUM::Token)
}
.onEach { items ->
state.update { HotCryptoUM(items = it.buildItems(items)) }
}
.launchIn(modelScope)
}
private fun HotCryptoUM.buildItems(items: List<TokensListItemUM.Token>): ImmutableList<TokensListItemUM> =
buildList {
if (items.isNotEmpty()) {
@ -163,16 +121,6 @@ internal class HotCryptoModel @Inject constructor(
)
}
private fun onTokenClick(tokenItemState: TokenItemState, currency: HotCryptoCurrency) {
bottomSheetNavigation.activate(
configuration = OnrampAddToPortfolioBSConfig.AddToPortfolio(
cryptoCurrency = currency.cryptoCurrency,
currencyIconState = tokenItemState.iconState,
onSuccessAdding = ::onSuccessAdding,
),
)
}
private fun startAddTokenFlow(currency: HotCryptoCurrency, hotCryptoPortfolioData: HotCryptoPortfolioData) {
hotCryptoToAddDataFlow.resetReplayCache()
fun closeNavigationFlow() = bottomSheetNavigationV2.replaceAll(OnrampAddTokenRoute.Empty)
@ -200,7 +148,7 @@ internal class HotCryptoModel @Inject constructor(
bottomSheetNavigationV2.replaceAll(OnrampAddTokenRoute.PortfolioSelector)
val tokenToAddStateFlow = portfolioSelectorController
.selectedAccountWithData(requireNotNull(portfolioFetcher))
.selectedAccountWithData(portfolioFetcher)
.filterNotNull()
.map { (_, selectedAccount) ->
val cryptoCurrency = updateCryptoCurrency(
@ -246,20 +194,6 @@ internal class HotCryptoModel @Inject constructor(
.saveIn(addHotCryptoJob)
}
private fun onSuccessAdding(id: CryptoCurrency.ID) {
modelScope.launch {
getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(
userWalletId = params.userWalletId,
cryptoCurrencyId = id,
)
.onRight { status ->
bottomSheetNavigation.dismiss()
params.onTokenClick(status)
}
.onLeft { Timber.d("Unable to get CryptoCurrencyStatus[$id]: $it") }
}
}
private fun setupPortfolioSelector(hotCrypto: HotCryptoCurrency, hotCryptoPortfolioData: HotCryptoPortfolioData) {
portfolioSelectorController.selectAccount(null)
portfolioSelectorController.isEnabled.value = isEnabled@{ _, accountStatus ->

View file

@ -1,34 +1,64 @@
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 com.tangem.features.onramp.tokenlist.entity.utils.addHeader
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
/**
[REDACTED_AUTHOR]
*/
internal class SetNoAvailablePairsTransformer(
private val appCurrency: AppCurrency,
private val unavailableStatuses: List<CryptoCurrencyStatus>,
private val accountList: Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>>,
private val isBalanceHidden: Boolean,
private val unavailableTokensHeaderReference: TextReference,
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 unavailableItems = OnrampTokenItemStateConverterFactory.createUnavailableItemConverter(appCurrency)
.convertList(unavailableStatuses)
.map(TokensListItemUM::Token)
val totalTokensCount = accountList.values.sumOf { it.size }
return prevState.copy(
availableItems = persistentListOf(),
unavailableItems = unavailableItems.addHeader(textReference = unavailableTokensHeaderReference),
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,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 SetNoAvailablePairsTransformerV2(
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

@ -13,10 +13,7 @@ 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.capitalize
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
@ -26,7 +23,6 @@ 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.getOrElse
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
@ -41,7 +37,6 @@ 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.tokens.GetTokenListUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
@ -51,9 +46,7 @@ import com.tangem.features.feed.components.market.details.portfolio.add.AddToPor
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.SetLoadingTokenItemsTransformer
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformerV2
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
@ -61,7 +54,9 @@ 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.*
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
@ -87,7 +82,6 @@ internal class AvailableSwapPairsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getTokenListUseCase: GetTokenListUseCase,
private val tokenListUMController: TokenListUMController,
private val searchManager: InputManager,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
@ -95,7 +89,6 @@ internal class AvailableSwapPairsModel @Inject constructor(
private val getAvailablePairsUseCase: GetAvailablePairsUseCase,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
private val excludedBlockchains: ExcludedBlockchains,
@ -120,7 +113,6 @@ internal class AvailableSwapPairsModel @Inject constructor(
}
private val addToPortfolioJobHolder = JobHolder()
private val tokenListFlow = getTokenListUseCaseFlow()
private val accountListFlow = getAccountListUseCaseFlow()
private val availablePairsByNetworkFlow = MutableStateFlow<Map<LeastTokenInfo, AvailablePairsState>>(emptyMap())
@ -157,11 +149,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
private val visibleDefaultMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
init {
if (accountsFeatureToggles.isFeatureEnabled) {
subscribeOnUpdateStateV2()
} else {
subscribeOnUpdateState()
}
subscribeOnUpdateState()
initializeSearchBarCallbacks()
subscribeOnSelectedStatusChange()
@ -173,18 +161,6 @@ internal class AvailableSwapPairsModel @Inject constructor(
}
}
private fun getTokenListUseCaseFlow(): SharedFlow<List<CryptoCurrencyStatus>> {
return 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 fun getAccountListUseCaseFlow(): SharedFlow<List<AccountStatus>> {
return singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(params.userWalletId))
.distinctUntilChanged()
@ -214,42 +190,6 @@ internal class AvailableSwapPairsModel @Inject constructor(
}
private fun subscribeOnUpdateState() {
combine(
flow = tokenListFlow,
flow2 = getAppCurrencyAndBalanceHidingFlow(),
flow3 = params.selectedStatus,
flow4 = searchManager.query,
flow5 = availablePairsByNetworkFlow
.map { it[params.selectedStatus.value?.toLeastTokenInfo()] }
.distinctUntilChanged(),
) { currencies, appCurrencyAndBalanceHiding, selectedStatus, query, availablePairsState ->
availablePairsState?.fold(
ifLoading = { SetLoadingTokenItemsTransformer(currencies) },
ifContent = { pairs ->
handleContentState(
appCurrencyAndBalanceHiding = appCurrencyAndBalanceHiding,
currencies = currencies,
selectedStatus = selectedStatus,
query = query,
availablePairs = pairs,
)
},
ifError = { throwable ->
handleErrorState(
cause = throwable,
networkInfo = params.selectedStatus.value?.toLeastTokenInfo(),
currencies = currencies,
)
},
)
?: SetLoadingTokenItemsTransformer(currencies)
}
.onEach(tokenListUMController::update)
.flowOn(dispatchers.main)
.launchIn(modelScope)
}
private fun subscribeOnUpdateStateV2() {
combine(
flow = getAccountsAndModeFlow(),
flow2 = getAppCurrencyAndBalanceHidingFlow(),
@ -269,7 +209,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
)
},
ifContent = { pairs ->
handleContentStateV2(
handleContentState(
appCurrencyAndBalanceHiding = appCurrencyAndBalanceHiding,
accountList = accountList,
selectedStatus = selectedStatus,
@ -279,7 +219,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
)
},
ifError = { throwable ->
handleErrorStateV2(
handleErrorState(
cause = throwable,
networkInfo = params.selectedStatus.value?.toLeastTokenInfo(),
accountList = accountList,
@ -297,52 +237,6 @@ internal class AvailableSwapPairsModel @Inject constructor(
}
private fun handleContentState(
appCurrencyAndBalanceHiding: Pair<AppCurrency, Boolean>,
currencies: List<CryptoCurrencyStatus>,
selectedStatus: CryptoCurrencyStatus?,
query: String,
availablePairs: List<SwapPairLeast>,
): TokenListUMTransformer {
val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding
if (availablePairs.isEmpty()) {
return SetNoAvailablePairsTransformer(
appCurrency = appCurrency,
unavailableStatuses = currencies,
isBalanceHidden = isBalanceHidden,
unavailableTokensHeaderReference = resourceReference(
id = R.string.tokens_list_unavailable_to_swap_header,
wrappedList(selectedStatus?.currency?.name?.capitalize().orEmpty()),
),
)
}
val filterByQueryTokenList = currencies
.filter { it.currency != selectedStatus?.currency }
.filterByQuery(query = query)
return if (query.isNotEmpty() && filterByQueryTokenList.isEmpty()) {
SetNothingToFoundStateTransformer(
isBalanceHidden = isBalanceHidden,
emptySearchMessageReference = resourceReference(
id = R.string.action_buttons_swap_empty_search_message,
),
)
} else {
UpdateTokenItemsTransformer(
appCurrency = appCurrency,
onItemClick = ::onPortfolioTokenClick,
statuses = filterByQueryTokenList.filterByAvailability(availablePairs = availablePairs),
isBalanceHidden = isBalanceHidden,
unavailableTokensHeaderReference = resourceReference(
id = R.string.tokens_list_unavailable_to_swap_header,
wrappedList(selectedStatus?.currency?.name?.capitalize().orEmpty()),
),
)
}
}
private fun handleContentStateV2(
appCurrencyAndBalanceHiding: Pair<AppCurrency, Boolean>,
accountList: List<AccountStatus>,
selectedStatus: CryptoCurrencyStatus?,
@ -371,7 +265,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
.filterValues { it.isNotEmpty() }
if (availablePairs.isEmpty()) {
return SetNoAvailablePairsTransformerV2(
return SetNoAvailablePairsTransformer(
appCurrency = appCurrency,
accountList = filterByQueryAccountList,
unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header),
@ -381,7 +275,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
}
return if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) {
SetNothingToFoundStateTransformerV2(
SetNothingToFoundStateTransformer(
isBalanceHidden = isBalanceHidden,
emptySearchMessageReference = resourceReference(
id = R.string.action_buttons_swap_empty_search_message,
@ -400,23 +294,6 @@ internal class AvailableSwapPairsModel @Inject constructor(
}
private fun handleErrorState(
cause: Throwable,
networkInfo: LeastTokenInfo?,
currencies: List<CryptoCurrencyStatus>,
): SetErrorWarningTransformer {
return SetErrorWarningTransformer(
cause = cause,
onRefresh = {
modelScope.launch {
if (networkInfo != null) {
updateAvailablePairs(networkInfo, currencies)
}
}
},
)
}
private fun handleErrorStateV2(
cause: Throwable,
networkInfo: LeastTokenInfo?,
accountList: List<AccountStatus>,
@ -450,19 +327,14 @@ internal class AvailableSwapPairsModel @Inject constructor(
val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() == true
if (isAlreadyLoaded) return@collectLatest
if (accountsFeatureToggles.isFeatureEnabled) {
val accountList = accountListFlow.firstOrNull() ?: return@collectLatest
updateAvailablePairs(
networkInfo = networkInfo,
statuses = accountList.filterCryptoPortfolio()
.flatMap { accountStatus ->
accountStatus.flattenCurrencies()
}.toSet().toList(),
)
} else {
val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest
updateAvailablePairs(networkInfo = networkInfo, statuses = statuses)
}
val accountList = accountListFlow.firstOrNull() ?: return@collectLatest
updateAvailablePairs(
networkInfo = networkInfo,
statuses = accountList.filterCryptoPortfolio()
.flatMap { accountStatus ->
accountStatus.flattenCurrencies()
}.toSet().toList(),
)
}
}
}
@ -540,19 +412,6 @@ internal class AvailableSwapPairsModel @Inject constructor(
}
}
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 &&
!status.currency.isCustom
}
}
private fun Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>>.filterByAvailability(
availablePairs: List<SwapPairLeast>,
): List<AccountAvailabilityUM> {

View file

@ -8,7 +8,6 @@ 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.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
@ -37,7 +36,6 @@ internal class SwapSelectTokensModel @Inject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
) : Model() {
val state: StateFlow<SwapSelectTokensUM> = controller.state
@ -77,14 +75,10 @@ internal class SwapSelectTokensModel @Inject constructor(
selectedTokenItemState = selectedTokenItemState,
onRemoveClick = ::onRemoveFromTokenClick,
isAccountsMode = isAccountsMode,
account = if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = params.userWalletId,
currency = status.currency,
).getOrNull()?.account
} else {
null
},
account = getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = params.userWalletId,
currency = status.currency,
).getOrNull()?.account,
),
)
}
@ -108,14 +102,10 @@ internal class SwapSelectTokensModel @Inject constructor(
transformer = SelectToTokenTransformer(
selectedTokenItemState = selectedTokenItemState,
isAccountsMode = isAccountsMode,
account = if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = params.userWalletId,
currency = status.currency,
).getOrNull()?.account
} else {
null
},
account = getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = params.userWalletId,
currency = status.currency,
).getOrNull()?.account,
),
)

View file

@ -2,8 +2,6 @@ package com.tangem.features.onramp.tokenlist.entity.transformer
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
@ -16,22 +14,18 @@ internal class SetNothingToFoundStateTransformer(
override fun transform(prevState: TokenListUM): TokenListUM {
return prevState.copy(
availableItems = persistentListOf(
createGroupTitle(
textReference = resourceReference(id = R.string.exchange_tokens_available_tokens_header),
),
TokensListItemUM.Text(
id = emptySearchMessageReference.hashCode(),
text = emptySearchMessageReference,
),
),
availableItems = persistentListOf(),
unavailableItems = persistentListOf(),
tokensListData = TokenListUMData.EmptyList,
tokensListData = TokenListUMData.TokenList(
tokensList = persistentListOf(
TokensListItemUM.Text(
id = emptySearchMessageReference.hashCode(),
text = emptySearchMessageReference,
),
),
totalTokensCount = 0,
),
isBalanceHidden = isBalanceHidden,
)
}
private fun createGroupTitle(textReference: TextReference): TokensListItemUM.GroupTitle {
return TokensListItemUM.GroupTitle(id = textReference.hashCode(), text = textReference)
}
}

View file

@ -1,31 +0,0 @@
package com.tangem.features.onramp.tokenlist.entity.transformer
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
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 kotlinx.collections.immutable.persistentListOf
internal class SetNothingToFoundStateTransformerV2(
private val isBalanceHidden: Boolean,
private val emptySearchMessageReference: TextReference,
) : TokenListUMTransformer {
override fun transform(prevState: TokenListUM): TokenListUM {
return prevState.copy(
availableItems = persistentListOf(),
unavailableItems = persistentListOf(),
tokensListData = TokenListUMData.TokenList(
tokensList = persistentListOf(
TokensListItemUM.Text(
id = emptySearchMessageReference.hashCode(),
text = emptySearchMessageReference,
),
),
totalTokensCount = 0,
),
isBalanceHidden = isBalanceHidden,
)
}
}

View file

@ -8,7 +8,6 @@ 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.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
@ -16,19 +15,14 @@ import com.tangem.domain.account.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.core.lce.Lce
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.tokens.GetAssetRequirementsUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.onramp.impl.R
@ -36,7 +30,9 @@ import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
import com.tangem.features.onramp.swap.entity.AccountCurrencyUM
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
import com.tangem.features.onramp.tokenlist.entity.*
import com.tangem.features.onramp.tokenlist.entity.transformer.*
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.utils.ClearSearchBarTransformer
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer
@ -60,12 +56,10 @@ internal class OnrampTokenListModel @Inject constructor(
private val searchManager: InputManager,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getTokenListUseCase: GetTokenListUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val rampStateManager: RampStateManager,
private val getUserCountryUseCase: GetUserCountryUseCase,
private val getAssetRequirementsUseCase: GetAssetRequirementsUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
) : Model() {
@ -84,68 +78,10 @@ internal class OnrampTokenListModel @Inject constructor(
onActiveChange = ::onSearchBarActiveChange,
),
)
if (accountsFeatureToggles.isFeatureEnabled) {
subscribeOnUpdateStateV2()
} else {
subscribeOnUpdateState()
}
subscribeOnUpdateState()
}
private fun subscribeOnUpdateState() {
combine(
flow = getTokenListUseCase.launch(userWalletId = params.userWalletId).distinctUntilChanged(),
flow2 = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(),
flow3 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(),
flow4 = searchManager.query,
flow5 = hasRestrictionForSellFlow(),
) { maybeTokenList, appCurrency, isBalanceHidden, query, hasRestrictionForSell ->
val currencies = maybeTokenList.getOrElse(
ifLoading = { it ?: TokenList.Empty },
ifError = { TokenList.Empty },
)
.flattenCurrencies()
val filterByQueryTokenList = currencies
.filterByQuery(query = query)
if (query.isNotEmpty() && filterByQueryTokenList.isEmpty()) {
SetNothingToFoundStateTransformer(
isBalanceHidden = isBalanceHidden,
emptySearchMessageReference = getEmptySearchMessageReference(),
)
} else {
val isInsufficientBalanceForSell = if (params.filterOperation == OnrampOperation.SELL) {
maybeTokenList.isInsufficientBalanceForSell()
} else {
false
}
UpdateTokenItemsTransformer(
appCurrency = appCurrency,
onItemClick = ::onTokenClick,
statuses = filterByQueryTokenList.let { statuses ->
if (hasRestrictionForSell || isInsufficientBalanceForSell) {
mapOf(false to statuses)
} else {
statuses.filterByAvailability()
}
},
isBalanceHidden = isBalanceHidden,
unavailableTokensHeaderReference = getUnavailableTokensHeaderReference(),
warning = when {
hasRestrictionForSell -> NotificationUM.Warning.SellingRegionalRestriction
isInsufficientBalanceForSell -> NotificationUM.Warning.InsufficientBalanceForSelling
else -> null
},
)
}
}
.onEach(::updateTokenListUM)
.flowOn(dispatchers.main)
.launchIn(modelScope)
}
private fun subscribeOnUpdateStateV2() {
combine(
flow = singleAccountStatusListSupplier(
SingleAccountStatusListProducer.Params(params.userWalletId),
@ -160,7 +96,7 @@ internal class OnrampTokenListModel @Inject constructor(
if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) {
updateTokenListUM(
SetNothingToFoundStateTransformerV2(
SetNothingToFoundStateTransformer(
isBalanceHidden = isBalanceHidden,
emptySearchMessageReference = getEmptySearchMessageReference(),
),
@ -211,16 +147,6 @@ internal class OnrampTokenListModel @Inject constructor(
}
}
private fun Lce<TokenListError, TokenList>.isInsufficientBalanceForSell(): Boolean {
return if (params.filterOperation == OnrampOperation.SELL) {
isContent {
(it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() == true
}
} else {
false
}
}
private fun AccountStatusList.isInsufficientBalanceForSell(): Boolean {
return if (params.filterOperation == OnrampOperation.SELL) {
(totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() == true
@ -268,13 +194,8 @@ internal class OnrampTokenListModel @Inject constructor(
}
private fun isFirstInitialization(prevState: TokenListUM, newState: TokenListUM): Boolean {
return if (accountsFeatureToggles.isFeatureEnabled) {
prevState.tokensListData == TokenListUMData.EmptyList &&
newState.tokensListData != TokenListUMData.EmptyList
} else {
prevState.availableItems.isEmpty() && prevState.unavailableItems.isEmpty() &&
(newState.availableItems.isNotEmpty() || newState.unavailableItems.isNotEmpty())
}
return prevState.tokensListData == TokenListUMData.EmptyList &&
newState.tokensListData != TokenListUMData.EmptyList
}
private fun onTokenClick(tokenItemState: TokenItemState, status: CryptoCurrencyStatus) {
@ -333,41 +254,6 @@ internal class OnrampTokenListModel @Inject constructor(
}
}
private suspend fun List<CryptoCurrencyStatus>.filterByAvailability(): Map<Boolean, List<CryptoCurrencyStatus>> {
return coroutineScope {
map { status ->
async {
val isOperationAvailable = checkAvailabilityByOperation(status = status)
val isNotMissedDerivation = status.value !is CryptoCurrencyStatus.MissedDerivation
val isNotLoading = status.value !is CryptoCurrencyStatus.Loading
val requirements = getAssetRequirementsUseCase(
userWalletId = userWallet.walletId,
currency = status.currency,
).getOrNull()
val isAvailableForBuy = rampStateManager.checkAssetRequirements(requirements)
val isNotUnreachable = status.value !is CryptoCurrencyStatus.Unreachable
val isAvailable = when (params.filterOperation) {
OnrampOperation.BUY -> {
isAvailableForBuy
} // unreachable state is available for Buy operation
OnrampOperation.SELL -> isNotUnreachable
OnrampOperation.SWAP -> {
isNotUnreachable && isAvailableForBuy
}
}
status to (isOperationAvailable && isNotMissedDerivation && isNotLoading && isAvailable)
}
}
.awaitAll()
.groupBy(Pair<CryptoCurrencyStatus, Boolean>::second)
.mapValues { it.value.map(Pair<CryptoCurrencyStatus, Boolean>::first) }
}
}
private suspend fun AccountCryptoList.filterByAvailability(): List<AccountAvailabilityUM> {
return coroutineScope {
map { (account, currencies) ->