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

@ -17,8 +17,6 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.card.common.extensions.canHandleBlockchain
import com.tangem.domain.card.common.extensions.canHandleToken
import com.tangem.domain.common.wallets.UserWalletsListRepository
@ -43,7 +41,6 @@ import timber.log.Timber
*
* @property excludedBlockchains excluded blockchains
* @property hotCryptoResponseStore store of `HotCryptoResponse`
* @property userWalletsStore store of `UserWallet`
* @property tangemTechApi tangem tech api
* @property appCurrencyResponseStore store of current app currency
* @property dispatchers dispatchers
@ -59,8 +56,6 @@ internal class DefaultHotCryptoRepository(
private val userWalletsListRepository: UserWalletsListRepository,
private val tangemTechApi: TangemTechApi,
private val appCurrencyResponseStore: AppCurrencyResponseStore,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val userTokensResponseStore: UserTokensResponseStore,
private val walletAccountsFetcher: WalletAccountsFetcher,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
@ -106,12 +101,8 @@ internal class DefaultHotCryptoRepository(
private fun getWalletsWithTokensFlow(): Flow<Map<UserWallet, List<UserTokensResponse.Token>>> {
return userWalletsListRepository.loadAndGet().flatMapLatest { userWallets ->
val flows = userWallets.map { userWallet ->
if (accountsFeatureToggles.isFeatureEnabled) {
walletAccountsFetcher.get(userWalletId = userWallet.walletId).map { it.toUserTokensResponse() }
} else {
userTokensResponseStore.get(userWalletId = userWallet.walletId)
}
.map { userWallet to it?.tokens.orEmpty() }
walletAccountsFetcher.get(userWalletId = userWallet.walletId).map { it.toUserTokensResponse() }
.map { userWallet to it.tokens }
}
combine(flows) { it.toMap() }

View file

@ -27,8 +27,6 @@ import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
import com.tangem.datasource.local.onramp.sepa.OnrampCurrentCountryByIPStore
import com.tangem.datasource.local.onramp.sepa.OnrampSepaAvailabilityStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.onramp.repositories.*
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -110,8 +108,6 @@ internal object OnrampDataModule {
appCurrencyResponseStore: AppCurrencyResponseStore,
dispatchers: CoroutineDispatcherProvider,
analyticsEventHandler: AnalyticsEventHandler,
userTokensResponseStore: UserTokensResponseStore,
accountsFeatureToggles: AccountsFeatureToggles,
walletAccountsFetcher: WalletAccountsFetcher,
): HotCryptoRepository {
return DefaultHotCryptoRepository(
@ -122,8 +118,6 @@ internal object OnrampDataModule {
appCurrencyResponseStore = appCurrencyResponseStore,
dispatchers = dispatchers,
analyticsEventHandler = analyticsEventHandler,
userTokensResponseStore = userTokensResponseStore,
accountsFeatureToggles = accountsFeatureToggles,
walletAccountsFetcher = walletAccountsFetcher,
)
}

View file

@ -20,7 +20,6 @@ import com.tangem.data.walletconnect.utils.WcNetworksConverter
import com.tangem.data.walletconnect.utils.WcScope
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.local.walletconnect.WalletConnectStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import com.tangem.domain.account.supplier.SingleAccountSupplier
@ -95,7 +94,6 @@ internal object WalletConnectDataModule {
getWallets: GetWalletsUseCase,
wcNetworksConverter: WcNetworksConverter,
analytics: AnalyticsEventHandler,
accountsFeatureToggles: AccountsFeatureToggles,
wcScope: WcScope,
): DefaultWcSessionsManager {
return DefaultWcSessionsManager(
@ -104,7 +102,6 @@ internal object WalletConnectDataModule {
getWallets = getWallets,
wcNetworksConverter = wcNetworksConverter,
analytics = analytics,
accountsFeatureToggles = accountsFeatureToggles,
scope = wcScope,
)
}

View file

@ -10,7 +10,6 @@ import com.reown.walletkit.client.Wallet
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.utils.WC_TAG
import com.tangem.data.walletconnect.utils.getDappOriginUrl
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.blockaid.BlockAidVerifier
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.model.*
@ -35,7 +34,6 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
private val sdkDelegate: WcPairSdkDelegate,
private val blockAidVerifier: BlockAidVerifier,
private val analytics: AnalyticsEventHandler,
private val accountsFeatureToggles: AccountsFeatureToggles,
@Assisted private val pairRequest: WcPairRequest,
) : WcPairUseCase {
@ -128,7 +126,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
pendingSessionForSave = pendingSessionForSave,
sessionForApprove = sessionForApprove,
sdkSessionProposal = sdkSessionProposal,
).map { settledSession ->
).map { _ ->
analytics.send(
WcAnalyticEvents.DAppConnected(
sessionProposal = proposalState.dAppSession,
@ -198,11 +196,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
verifyContext: Wallet.Model.VerifyContext,
): Either<WcPairError, WcPairState.Proposal> = runCatching {
val proposalNetwork = associateNetworksDelegate.associate(sessionProposal)
val proposalAccountNetwork = if (accountsFeatureToggles.isFeatureEnabled) {
associateNetworksDelegate.associateAccounts(sessionProposal)
} else {
null
}
val proposalAccountNetwork = associateNetworksDelegate.associateAccounts(sessionProposal)
val verificationInfo = when {
verifyContext.validation == Wallet.Model.Validation.INVALID -> CheckDAppResult.UNSAFE
verifyContext.isScam == true -> CheckDAppResult.UNSAFE
@ -211,7 +205,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
CheckDAppResult.FAILED_TO_VERIFY
}
}
val requestedNetworks = (proposalAccountNetwork ?: proposalNetwork)
val requestedNetworks = proposalAccountNetwork
.values.map { it.available.plus(it.required) }.flatten().toSet()
analytics.send(
WcAnalyticEvents.PairRequested(

View file

@ -8,7 +8,6 @@ import com.reown.walletkit.client.WalletKit
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.utils.*
import com.tangem.datasource.local.walletconnect.WalletConnectStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
@ -34,7 +33,6 @@ internal class DefaultWcSessionsManager(
private val dispatchers: CoroutineDispatcherProvider,
private val wcNetworksConverter: WcNetworksConverter,
private val analytics: AnalyticsEventHandler,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val scope: WcScope,
) : WcSessionsManager, WcSdkObserver {
@ -57,7 +55,6 @@ internal class DefaultWcSessionsManager(
.flowOn(dispatchers.io)
private suspend fun migrateToAccountSession(inStore: Set<WcSessionDTO>): Boolean {
if (!accountsFeatureToggles.isFeatureEnabled) return false
if (oneTimeMigration.value) return false
var someMigrated = false
@ -118,9 +115,7 @@ internal class DefaultWcSessionsManager(
val wallet = wallets.find { it.walletId == storeSession.walletId } ?: return@mapNotNull null
val sdkSession = inSdk.find { it.topic == storeSession.topic } ?: return@mapNotNull null
val account = storeSession.accountId?.let { wcNetworksConverter.getAccount(it) } as? Account.CryptoPortfolio
if (accountsFeatureToggles.isFeatureEnabled && account == null) {
return@mapNotNull null
}
?: return@mapNotNull null
val networks = wcNetworksConverter.findWalletNetworks(wallet, account, sdkSession)
val originUrl = storeSession.url ?: sdkSession.metaData?.url ?: ""
WcSession(

View file

@ -14,7 +14,6 @@ import com.tangem.data.walletconnect.pair.CaipNamespaceDelegate
import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase
import com.tangem.data.walletconnect.pair.WcPairSdkDelegate
import com.tangem.data.walletconnect.utils.WcSdkSessionConverter
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.blockaid.BlockAidVerifier
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletconnect.model.WcPairError
@ -37,7 +36,6 @@ internal class DefaultWcPairUseCaseTest {
private val analytics: AnalyticsEventHandler = mockk<AnalyticsEventHandler>(relaxed = true)
private val sdkDelegate: WcPairSdkDelegate = mockk<WcPairSdkDelegate>()
private val blockAidVerifier: BlockAidVerifier = mockk<BlockAidVerifier>()
private val accountsFeatureToggles = mockk<AccountsFeatureToggles>()
private val url = "testUrl"
private val source = WcPairRequest.Source.QR
@ -117,14 +115,13 @@ internal class DefaultWcPairUseCaseTest {
sdkDelegate = sdkDelegate,
blockAidVerifier = blockAidVerifier,
analytics = analytics,
accountsFeatureToggles = accountsFeatureToggles,
pairRequest = WcPairRequest(userWalletId = UserWalletId(""), uri = url, source = source),
)
@Before
fun setup() {
coEvery { associateNetworksDelegate.associate(sdkProposal) } returns mapOf()
coEvery { accountsFeatureToggles.isFeatureEnabled } returns false
coEvery { associateNetworksDelegate.associateAccounts(sdkProposal) } returns mapOf()
coEvery {
caipNamespaceDelegate.associate(
sessionProposal = sdkProposal,

View file

@ -1,5 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues/>
</SmellBaseline>

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) ->

View file

@ -27,12 +27,10 @@
<ID>MultilineLambdaItParameter:WcSwitchNetworkModel.kt$WcSwitchNetworkModel${ if (it.isExistInWcSession) { router.pop() } else { showErrorDialog(HandleMethodError.RequiredNetwork(it.network.name)) } }</ID>
<ID>NamedArguments:WcAddEthereumChainModalBottomSheetContent.kt$WcAddEthereumChainModalBottomSheetContent(state, {}, {}, {})</ID>
<ID>NamedArguments:WcAlertsFactory.kt$WcAlertsFactory$createMaliciousDAppAlert(alertType.description, alertType.onClick, alertType.iconType, alertType.iconBgType)</ID>
<ID>NamedArguments:WcPairModel.kt$WcPairModel$handlePairState( pairState, portfolios, selected, isAccountMode, )</ID>
<ID>NamedArguments:WcSendTransactionModel.kt$WcSendTransactionModel$buildUiState(securityCheck, useCase, signState, isApprovalMethod)</ID>
<ID>NestedScopeFunctions:WcSendAndReceiveBlockAidUiConverter.kt$WcSendAndReceiveBlockAidUiConverter$let { spendAllowanceUMConverter.convert( WcSpendAllowanceUMConverter.Input( approvedAmount = it, onLearnMoreClick = value.onApproveLearnMoreClick, ), ) }</ID>
<ID>NoNameShadowing:WcNavigationUtils.kt$model</ID>
<ID>NullCheckOnMutableProperty:WcCommonTransactionComponentDelegate.kt$WcCommonTransactionComponentDelegate$if (contentStack != null) { val content by contentStack!!.subscribeAsState() BackHandler(onBack = ::onChildBack) content.active.instance.BottomSheet() }</ID>
<ID>NullableBooleanCheck:WcPairModel.kt$WcPairModel$isAccountMode ?: false</ID>
<ID>NullableToStringCall:WcEstimatedWalletChangeUMConverter.kt$WcEstimatedWalletChangeUMConverter$${value.sign}</ID>
<ID>ReusedModifierInstance:DefaultWalletConnectEntryComponent.kt$DefaultWalletConnectEntryComponent$Content(modifier = modifier)</ID>
<ID>ReusedModifierInstance:WcAppInfoBS.kt$Box( modifier = modifier .padding(start = 48.dp) .border( width = 2.dp, color = TangemTheme.colors.background.action, shape = CircleShape, ) .padding(2.dp) .background(color = TangemTheme.colors.background.action) .size(20.dp) .clip(CircleShape) .background(color = TangemTheme.colors.icon.primary1.copy(alpha = 0.1F)), ) { Text( modifier = Modifier.align(Alignment.Center), text = "+$remainingCount", style = TangemTheme.typography.overline, color = TangemTheme.colors.text.secondary, ) }</ID>
@ -46,7 +44,6 @@
<ID>UnsafeCallOnNullableType:WcPairComponent.kt$WcPairComponent$model.portfolioFetcher!!</ID>
<ID>UnsafeCallOnNullableType:WcSignTransactionComponent.kt$WcSignTransactionComponent$content!!</ID>
<ID>UnsafeCallOnNullableType:WcTransactionRequestInfoComponent.kt$WcTransactionRequestInfoComponent$content!!</ID>
<ID>UseEmptyCounterpart:WcPairModel.kt$WcPairModel$setOf&lt;Network&gt;()</ID>
<ID>UseOrEmpty:WcSpendAllowanceUMConverter.kt$WcSpendAllowanceUMConverter$value.approvedAmount.amount?.currencySymbol ?: ""</ID>
<ID>UseOrEmpty:WcTransactionCheckErrorItem.kt$notification.text?.resolveReference() ?: ""</ID>
</CurrentIssues>

View file

@ -15,11 +15,9 @@ import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.walletconnect.connections.components.WcSelectNetworksComponent.*
import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent.*
import com.tangem.features.walletconnect.connections.model.WcPairModel
import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes
import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes.Alert
@ -115,7 +113,7 @@ internal class WcPairComponent(
WcAppInfoRoutes.PortfolioSelector -> portfolioSelectorComponentFactory.create(
context = appComponentContext,
params = PortfolioSelectorComponent.Params(
portfolioFetcher = model.portfolioFetcher!!,
portfolioFetcher = model.portfolioFetcher,
bsCallback = model.portfolioSelectorCallback,
controller = model.selectorController,
),

View file

@ -18,7 +18,6 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.qrscanning.models.QrResultSource
@ -55,7 +54,6 @@ internal class WcConnectionsModel @Inject constructor(
private val wcDisconnectUseCase: WcDisconnectUseCase,
private val multiAccountListSupplier: MultiAccountListSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
accountsFeatureToggles: AccountsFeatureToggles,
private val wcPairService: WcPairService,
override val dispatchers: CoroutineDispatcherProvider,
analytics: AnalyticsEventHandler,
@ -70,11 +68,7 @@ internal class WcConnectionsModel @Inject constructor(
init {
analytics.send(WcAnalyticEvents.ScreenOpened())
listenQrUpdates()
if (accountsFeatureToggles.isFeatureEnabled) {
listenWcSessions()
} else {
listenWcSessionsOld()
}
listenWcSessions()
}
private fun listenQrUpdates() {
@ -98,21 +92,6 @@ internal class WcConnectionsModel @Inject constructor(
.launchIn(modelScope)
}
private fun listenWcSessionsOld() {
wcSessionsUseCase.invoke()
.conflate()
.distinctUntilChanged()
.onEach { sessionsMap ->
uiState.update(
WcSessionsTransformer(
sessionsMap = sessionsMap,
openAppInfoModal = ::openAppInfoModal,
),
)
}
.launchIn(modelScope)
}
private fun listenWcSessions() {
combine(
flow = wcSessionsUseCase.invoke().distinctUntilChanged(),

View file

@ -21,7 +21,6 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
@ -75,7 +74,6 @@ internal class WcPairModel @Inject constructor(
private val messageSender: UiMessageSender,
override val dispatchers: CoroutineDispatcherProvider,
private val analytics: AnalyticsEventHandler,
private val accountsFeatureToggles: AccountsFeatureToggles,
val selectorController: PortfolioSelectorController,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
@ -95,7 +93,10 @@ internal class WcPairModel @Inject constructor(
)
val stackNavigation = StackNavigation<WcAppInfoRoutes>()
val portfolioFetcher: PortfolioFetcher?
val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true),
scope = modelScope,
)
val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback {
override val onDismiss: () -> Unit = { stackNavigation.pop() }
override val onBack: () -> Unit = { stackNavigation.pop() }
@ -110,32 +111,23 @@ internal class WcPairModel @Inject constructor(
)
private var proposalNetwork by Delegates.notNull<WcSessionProposal.ProposalNetwork>()
private var sessionProposal by Delegates.notNull<WcSessionProposal>()
private var additionallyEnabledNetworks = setOf<Network>()
private var additionallyEnabledNetworks = emptySet<Network>()
private val dAppVerifiedStateConverter = WcDAppVerifiedStateConverter(onVerifiedClick = ::showVerifiedAlert)
val appInfoUiState: StateFlow<WcAppInfoUM>
field = MutableStateFlow<WcAppInfoUM>(createLoadingState())
init {
if (accountsFeatureToggles.isFeatureEnabled) {
portfolioFetcher = portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true),
scope = modelScope,
)
modelScope.launch {
val params = SingleAccountListProducer.Params(params.userWalletId)
val accountList = singleAccountListSupplier.getSyncOrNull(params)
if (accountList == null) {
router.pop()
return@launch
}
val firstAccount = accountList.accounts.first()
selectorController.selectAccount(firstAccount.accountId)
combineFlows(portfolioFetcher)
modelScope.launch {
val params = SingleAccountListProducer.Params(params.userWalletId)
val accountList = singleAccountListSupplier.getSyncOrNull(params)
if (accountList == null) {
router.pop()
return@launch
}
} else {
portfolioFetcher = null
loadDAppInfo()
val firstAccount = accountList.accounts.first()
selectorController.selectAccount(firstAccount.accountId)
combineFlows(portfolioFetcher)
}
}
@ -151,22 +143,16 @@ internal class WcPairModel @Inject constructor(
flow4 = isAccountsModeEnabledUseCase(),
transform = { portfolios, selected, pairState, isAccountMode ->
handlePairState(
pairState,
portfolios,
selected,
isAccountMode,
pairState = pairState,
portfolios = portfolios,
selected = selected,
isAccountMode = isAccountMode,
)
},
)
.launchIn(modelScope)
}
private fun loadDAppInfo() {
wcPairUseCase()
.onEach { pairState -> handlePairState(pairState) }
.launchIn(modelScope)
}
private suspend fun handlePairState(
pairState: WcPairState,
portfolios: PortfolioFetcher.Data? = null,
@ -193,7 +179,7 @@ internal class WcPairModel @Inject constructor(
)
processError(pairState.error)
}
is WcPairState.Loading -> appInfoUiState.update { createLoadingState(isAccountMode ?: false) }
is WcPairState.Loading -> appInfoUiState.update { createLoadingState(isAccountMode == true) }
is WcPairState.Proposal -> handleProposalState(
pairState = pairState,
portfolios = portfolios,
@ -225,7 +211,7 @@ internal class WcPairModel @Inject constructor(
} else {
val portfolioSelectRow = tryToCreatePortfolioSelectRow(selected, portfolios)
if (proposalAccountNetwork != null) {
selectorController.isEnabled.value = { wallet, account ->
selectorController.isEnabled.value = { _, account ->
proposalAccountNetwork.contains(account.account.accountId)
}
}