From 9ee1319e40358248f32a516380579062181a0738 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Mar 2026 06:24:04 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../portfolio/add/AddToPortfolioManager.kt | 1 + .../feature/swap/DefaultSwapComponent.kt | 50 +- .../choosetoken/api/ChooseTokenComponent.kt | 82 ++++ .../choosetoken/api/SettingContextUseCase.kt | 40 ++ .../impl/DefaultChooseTokenBridge.kt | 46 ++ .../impl/DefaultChooseTokenComponent.kt | 72 +++ .../choosetoken/impl/di/ChooseTokenModule.kt | 39 ++ .../impl/model/ChooseTokenModel.kt | 289 +++++++++++ .../swap/converters/TokensDataConverter.kt | 92 ++-- .../tangem/feature/swap/model/SwapModel.kt | 459 +++++------------- .../swap/models/SwapSelectTokenStateHolder.kt | 37 +- .../feature/swap/models/SwapStateHolder.kt | 1 - .../tangem/feature/swap/models/UiActions.kt | 2 - ...ager.kt => MarketsListBatchFlowManager.kt} | 35 +- .../models/market/state/SwapMarketState.kt | 15 +- .../tangem/feature/swap/ui/StateBuilder.kt | 34 -- .../feature/swap/ui/SwapSelectTokenScreen.kt | 178 +------ .../preview/SwapSelectTokenPreviewProvider.kt | 27 -- 18 files changed, 786 insertions(+), 713 deletions(-) create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/SettingContextUseCase.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/di/ChooseTokenModule.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt rename features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/{SwapMarketsListBatchFlowManager.kt => MarketsListBatchFlowManager.kt} (88%) diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioManager.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioManager.kt index 8d4e35e9ce..50b323e258 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioManager.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioManager.kt @@ -10,6 +10,7 @@ import kotlinx.serialization.Serializable interface AddToPortfolioManager { + // todo swap make updatable val token: TokenMarketParams val analyticsParams: AnalyticsParams? val portfolioFetcher: PortfolioFetcher diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index 6f5e860827..7035f014b0 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.background import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate @@ -14,23 +13,21 @@ import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.R -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent import com.tangem.feature.swap.model.SwapModel -import com.tangem.feature.swap.models.AddToPortfolioRoute import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.ui.SwapScreen -import com.tangem.feature.swap.ui.SwapSelectTokenScreen import com.tangem.feature.swap.ui.SwapSuccessScreen import com.tangem.features.approval.api.GiveApprovalComponent -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.swap.SwapComponent import com.tangem.utils.extensions.isZero @@ -45,19 +42,19 @@ internal class DefaultSwapComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: SwapComponent.Params, private val swapFeeSelectorBlockComponentFactory: SwapFeeSelectorBlockComponent.Factory, - private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, private val giveApprovalComponentFactory: GiveApprovalComponent.Factory, + private val chooseTokenComponentFactory: ChooseTokenComponent.Factory, ) : SwapComponent, AppComponentContext by appComponentContext { private val model: SwapModel = getOrCreateModel(params) - private val bottomSheetSlot = childSlot( - source = model.bottomSheetNavigation, - serializer = AddToPortfolioRoute.serializer(), - key = BOTTOM_SHEET_SLOT_KEY, - handleBackButton = false, - childFactory = { _, context -> bottomSheetChild(context) }, - ) + // todo swap create InnerRouter + private val chooseTokenComponent by lazy { + chooseTokenComponentFactory.create( + context = child("chooseTokenComponent"), + params = ChooseTokenComponent.Params(model.chooseTokenBridge), + ) + } private val approvalSlot = childSlot( key = APPROVAL_SLOT_KEY, @@ -161,7 +158,6 @@ internal class DefaultSwapComponent @AssistedInject constructor( val feeSelectorChildStackState by childSlot.subscribeAsState() val feeSelectorBlockComponent = feeSelectorChildStackState.child?.instance - val bottomSheet by bottomSheetSlot.subscribeAsState() Crossfade( modifier = Modifier.background(TangemTheme.colors.background.secondary), @@ -189,38 +185,14 @@ internal class DefaultSwapComponent @AssistedInject constructor( ) } } - SwapNavScreen.SelectToken -> { - val tokenState = model.uiState.selectTokenState - if (tokenState != null) { - SwapSelectTokenScreen(state = tokenState, onBack = model.uiState.onBackClicked) - } else { - SwapScreen( - stateHolder = model.uiState, - feeSelectorBlockComponent = feeSelectorBlockComponent, - ) - } - } + SwapNavScreen.SelectToken -> chooseTokenComponent.Content(Modifier) } } - bottomSheet.child?.instance?.BottomSheet() - val approvalSlotState by approvalSlot.subscribeAsState() approvalSlotState.child?.instance?.BottomSheet() } - @Suppress("UnsafeCallOnNullableType") - private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent { - return addToPortfolioComponentFactory.create( - context = childByContext(componentContext), - params = AddToPortfolioComponent.Params( - addToPortfolioManager = model.addToPortfolioManager!!, - callback = model.addToPortfolioCallback, - shouldSkipTokenActionsScreen = true, - ), - ) - } - fun getApprovalParams(): GiveApprovalComponent.Params? { val permissionState = model.uiState.permissionState as? GiveTxPermissionState.ReadyForRequest ?: return null diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt new file mode 100644 index 0000000000..9caef19998 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt @@ -0,0 +1,82 @@ +package com.tangem.feature.swap.choosetoken.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +// todo swap make universal, encapsulate, move to some common module +internal interface ChooseTokenBridge { + + // todo swap new api + val onCurrencyChosen: Channel + val onClose: Channel + + // todo swap legacy api, remove + val onTokenSelected: Channel> + val onNewTokenAdded: Channel> + + val searchQueryState: StateFlow + val currenciesGroup: Flow + + fun onTokenSelected(tokenId: Pair) { + onTokenSelected.trySend(tokenId) + onSearchQuery("") + } + + fun onNewTokenAdded(addedToken: Pair) { + onNewTokenAdded.trySend(addedToken) + onSearchQuery("") + } + + fun onSearchQuery(query: String) + + fun updateCurrenciesGroup(currenciesGroup: CurrenciesGroup) + + fun onCurrencyChosen(result: ChooseTokenResult) { + onCurrencyChosen.trySend(result) + } + + fun onClose() { + onClose.trySend(Unit) + onSearchQuery("") + } + + interface Factory { + fun create(modelScope: CoroutineScope): ChooseTokenBridge + } +} + +data class ChooseTokenResult( + val addedCurrency: AccountCryptoCurrencyStatus, + val userWallet: UserWallet, + val analyticsPayload: Set = emptySet(), +) { + val status: CryptoCurrencyStatus get() = addedCurrency.status + val account: Account.CryptoPortfolio get() = addedCurrency.account + val walletId get() = userWallet.walletId +} + +sealed interface ChooseTokenAnalyticsPayload { + + @Suppress("BooleanPropertyNaming") + @JvmInline + value class IsSearched(val value: Boolean) : ChooseTokenAnalyticsPayload +} + +internal interface ChooseTokenComponent : ComposableContentComponent { + + data class Params( + val bridge: ChooseTokenBridge, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/SettingContextUseCase.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/SettingContextUseCase.kt new file mode 100644 index 0000000000..a40a2a311c --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/SettingContextUseCase.kt @@ -0,0 +1,40 @@ +package com.tangem.feature.swap.choosetoken.api + +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 kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import javax.inject.Inject + +// todo swap move to some common module +class SettingContextUseCase @Inject constructor( + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, +) { + + suspend fun invokeSync(): SettingContext = invoke().first() + + operator fun invoke(): Flow = combine( + flow = isAccountsModeEnabledUseCase.invoke(), + flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), + flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(), + transform = { isAccountsModeEnabled, selectedAppCurrency, balanceHidingSettings -> + SettingContext( + isAccountsMode = isAccountsModeEnabled, + appCurrency = selectedAppCurrency, + isBalanceHidden = balanceHidingSettings, + ) + }, + ).distinctUntilChanged() +} + +data class SettingContext( + val isAccountsMode: Boolean, + val appCurrency: AppCurrency, + val isBalanceHidden: Boolean, +) \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt new file mode 100644 index 0000000000..870a5a7abe --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.swap.choosetoken.impl + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload +import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge +import com.tangem.feature.swap.choosetoken.api.ChooseTokenResult +import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel.Companion.DEBOUNCE_SEARCH_DELAY +import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.* + +internal class DefaultChooseTokenBridge @AssistedInject constructor( + @Assisted private val modelScope: CoroutineScope, +) : ChooseTokenBridge { + + override val onCurrencyChosen: Channel = Channel() + + override val onTokenSelected: Channel> = Channel() + override val onNewTokenAdded: Channel> = Channel() + override val onClose: Channel = Channel() + + private val onSearchQuery: Channel = Channel() + override val searchQueryState: StateFlow = onSearchQuery.receiveAsFlow() + .debounce(DEBOUNCE_SEARCH_DELAY) + .stateIn(modelScope, SharingStarted.Eagerly, initialValue = "") + + private val _currenciesGroupFlow = MutableStateFlow(null) + override val currenciesGroup: Flow = _currenciesGroupFlow.filterNotNull() + + override fun onSearchQuery(query: String) { + onSearchQuery.trySend(query) + } + + override fun updateCurrenciesGroup(currenciesGroup: CurrenciesGroup) { + _currenciesGroupFlow.update { currenciesGroup } + } + + @AssistedFactory + interface Factory : ChooseTokenBridge.Factory { + override fun create(modelScope: CoroutineScope): DefaultChooseTokenBridge + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt new file mode 100644 index 0000000000..3ac09c489f --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt @@ -0,0 +1,72 @@ +package com.tangem.feature.swap.choosetoken.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent +import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel +import com.tangem.feature.swap.models.AddToPortfolioRoute +import com.tangem.feature.swap.ui.SwapSelectTokenScreen +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultChooseTokenComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: ChooseTokenComponent.Params, + private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, +) : AppComponentContext by appComponentContext, ChooseTokenComponent { + + private val model: ChooseTokenModel = getOrCreateModel(params) + + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = AddToPortfolioRoute.serializer(), + key = BOTTOM_SHEET_SLOT_KEY, + handleBackButton = false, + childFactory = { _, context -> bottomSheetChild(context) }, + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() + state?.let { stateHolder -> + SwapSelectTokenScreen(state = stateHolder, onBack = { model.onBackClicked() }) + } + bottomSheet.child?.instance?.BottomSheet() + } + + @Suppress("UnsafeCallOnNullableType") + private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent { + return addToPortfolioComponentFactory.create( + context = childByContext(componentContext), + params = AddToPortfolioComponent.Params( + addToPortfolioManager = model.addToPortfolioManager!!, + callback = model.addToPortfolioCallback, + shouldSkipTokenActionsScreen = true, + ), + ) + } + + @AssistedFactory + interface Factory : ChooseTokenComponent.Factory { + override fun create( + context: AppComponentContext, + params: ChooseTokenComponent.Params, + ): DefaultChooseTokenComponent + } + + private companion object { + const val BOTTOM_SHEET_SLOT_KEY = "choosePortfolioTokenBottomSheetSlot" + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/di/ChooseTokenModule.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/di/ChooseTokenModule.kt new file mode 100644 index 0000000000..48e130b850 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/di/ChooseTokenModule.kt @@ -0,0 +1,39 @@ +package com.tangem.feature.swap.choosetoken.impl.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge +import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent +import com.tangem.feature.swap.choosetoken.impl.DefaultChooseTokenBridge +import com.tangem.feature.swap.choosetoken.impl.DefaultChooseTokenComponent +import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(ModelComponent::class) +internal interface ChooseTokenModelModule { + + @Binds + @IntoMap + @ClassKey(ChooseTokenModel::class) + fun provideChooseTokenModel(model: ChooseTokenModel): Model +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface ChooseTokenFeatureModule { + + @Binds + @Singleton + fun provideChooseTokenComponentFactory(impl: DefaultChooseTokenComponent.Factory): ChooseTokenComponent.Factory + + @Binds + @Singleton + fun provideDefaultChooseTokenBridgeFactory(impl: DefaultChooseTokenBridge.Factory): ChooseTokenBridge.Factory +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt new file mode 100644 index 0000000000..9348c16c67 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt @@ -0,0 +1,289 @@ +package com.tangem.feature.swap.choosetoken.impl.model + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources +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.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains +import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketListConfig +import com.tangem.domain.markets.toSerializableParam +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload +import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge +import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent +import com.tangem.feature.swap.choosetoken.api.SettingContextUseCase +import com.tangem.feature.swap.converters.TokensDataConverter +import com.tangem.feature.swap.models.AddToPortfolioRoute +import com.tangem.feature.swap.models.SwapSelectTokenStateHolder +import com.tangem.feature.swap.models.market.MarketsListBatchFlowManager +import com.tangem.feature.swap.models.market.state.SwapMarketState +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@ModelScoped +internal class ChooseTokenModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, + private val excludedBlockchains: ExcludedBlockchains, + private val getUserWalletsUseCase: GetWalletsUseCase, + private val settingContextUseCase: SettingContextUseCase, + private val marketsListBatchFlowManagerFactory: MarketsListBatchFlowManager.Factory, + paramsContainer: ParamsContainer, +) : Model() { + + private val params = paramsContainer.require() + private val bridge: ChooseTokenBridge = params.bridge + private val searchQueryState = bridge.searchQueryState + + private val addToPortfolioJobHolder = JobHolder() + + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + + private val visibleMarketItemIds = MutableStateFlow>(emptyList()) + private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) + + var addToPortfolioManager: AddToPortfolioManager? = null + val addToPortfolioCallback = object : AddToPortfolioComponent.Callback { + override fun onDismiss() = bottomSheetNavigation.dismiss() + + override fun onSuccess(addedToken: CryptoCurrency) { + modelScope.launch { + val newToken = addedToken to ChooseTokenAnalyticsPayload + .IsSearched(searchQueryState.value.isNotEmpty()) + bridge.onNewTokenAdded(newToken) + bottomSheetNavigation.dismiss() + } + } + } + + private val defaultMarketsListManager by lazy { + marketsListBatchFlowManagerFactory.create( + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, + order = TokenMarketListConfig.Order.Trending, + currentSearchText = Provider { null }, + modelScope = modelScope, + ) + } + + private val searchMarketsListManager by lazy { + marketsListBatchFlowManagerFactory.create( + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, + order = TokenMarketListConfig.Order.ByRating, + currentSearchText = Provider { searchQueryState.value }, + modelScope = modelScope, + ) + } + + val state: StateFlow = combineUI() + + init { + subscribeMarketTokens() + } + + private fun combineUI(): StateFlow = combine( + flow = bridge.currenciesGroup, + flow2 = settingContextUseCase.invoke(), + flow3 = marketsStateFlow(), + transform = { currenciesGroup, settingContext, marketState -> + val isAccountsMode = settingContext.isAccountsMode + val appCurrency = settingContext.appCurrency + val isBalanceHidden = settingContext.isBalanceHidden + TokensDataConverter( + onSearchEntered = { query -> bridge.onSearchQuery(query) }, + onTokenSelected = { tokenId -> + val selected = tokenId to ChooseTokenAnalyticsPayload + .IsSearched(searchQueryState.value.isNotEmpty()) + bridge.onTokenSelected(selected) + }, + tokensDataState = currenciesGroup, + isBalanceHidden = isBalanceHidden, + isAccountsMode = isAccountsMode, + appCurrency = appCurrency, + marketState = marketState, + ).transform() + }, + ) + .flowOn(dispatchers.default) + .stateIn(modelScope, SharingStarted.Eagerly, initialValue = null) + + private fun marketsStateFlow() = searchQueryState + // Switch between default and search market flows + .map { it.isEmpty() } + .distinctUntilChanged() + .flatMapLatest { isDefaultMode -> + if (isDefaultMode) { + visibleMarketItemIds.value = emptyList() + createDefaultMarketsFlow() + } else { + visibleDefaultMarketItemIds.value = emptyList() + createSearchMarketsFlow() + } + } + + private fun subscribeMarketTokens() { + // Reload search markets when query changes + searchQueryState + .onEach { searchQuery -> + if (searchQuery.isNotEmpty()) { + searchMarketsListManager.reload(searchQuery) + } + } + .launchIn(modelScope) + + // Initial load of default markets + defaultMarketsListManager.reload() + + visibleMarketItemIds + .mapNotNull { rawIDS -> + if (rawIDS.isNotEmpty()) { + searchMarketsListManager.getBatchKeysByItemIds(rawIDS) + } else { + null + } + } + .distinctUntilChanged() + .transformLatest, Unit> { visibleBatchKeys -> + searchMarketsListManager.loadCharts(visibleBatchKeys) + } + .launchIn(modelScope) + + visibleDefaultMarketItemIds + .mapNotNull { rawIds -> + if (rawIds.isNotEmpty()) { + defaultMarketsListManager.getBatchKeysByItemIds(rawIds) + } else { + null + } + }.distinctUntilChanged() + .transformLatest, Unit> { visibleBatchKeys -> + defaultMarketsListManager.loadCharts(visibleBatchKeys) + } + .launchIn(modelScope) + } + + private fun createDefaultMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(R.string.feed_trending_now) + return combine( + defaultMarketsListManager.uiItems, + defaultMarketsListManager.isInInitialLoadingErrorState, + defaultMarketsListManager.totalCount, + ) { uiItems, isError, total -> + when { + isError -> SwapMarketState.LoadingError( + onRetryClicked = { defaultMarketsListManager.reload() }, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + uiItems.isEmpty() -> SwapMarketState.DefaultLoading + else -> SwapMarketState.Content( + items = uiItems, + loadMore = { defaultMarketsListManager.loadMore() }, + onItemClick = { item -> addToPortfolioItem(item) }, + visibleIdsChanged = { visibleDefaultMarketItemIds.value = it }, + total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + } + } + } + + private fun createSearchMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(R.string.markets_common_title) + return combine( + flow = searchMarketsListManager.uiItems, + flow2 = searchMarketsListManager.isInInitialLoadingErrorState, + flow3 = searchMarketsListManager.isSearchNotFoundState, + flow4 = searchMarketsListManager.totalCount, + ) { uiItems, isError, isSearchNotFound, total -> + when { + isError -> SwapMarketState.LoadingError( + onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value) }, + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + isSearchNotFound -> SwapMarketState.SearchNothingFound + uiItems.isEmpty() -> SwapMarketState.SearchLoading + else -> SwapMarketState.Content( + items = uiItems, + loadMore = { searchMarketsListManager.loadMore() }, + onItemClick = { item -> addToPortfolioItem(item) }, + visibleIdsChanged = { visibleMarketItemIds.value = it }, + total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + } + } + } + + private fun addToPortfolioItem(item: MarketsListItemUM) { + modelScope.launch { + val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) + ?: searchMarketsListManager.getTokenMarketById(item.id) + ?: return@launch + + val param = tokenMarket.toSerializableParam() + val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot } + + val networks = tokenMarket.networks?.filter { network -> + BlockchainUtils.isSupportedNetworkId( + blockchainId = network.networkId, + coinId = tokenMarket.id.value, + contractAddress = network.contractAddress, + excludedBlockchains = excludedBlockchains, + hotExcludedBlockchains = hotWalletExcludedBlockchains, + hasOnlyHotWallets = hasOnlyHotWallets, + ) + }?.map { network -> + TokenMarketInfo.Network( + networkId = network.networkId, + isExchangeable = false, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + }.orEmpty() + + addToPortfolioManager = addToPortfolioManagerFactory + .create( + scope = modelScope, + token = param, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value), + ).apply { + setTokenNetworks(networks) + } + + addToPortfolioManager?.state + ?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd } + ?.run { bottomSheetNavigation.activate(AddToPortfolioRoute) } + }.saveIn(addToPortfolioJobHolder) + } + + fun onBackClicked() { + bridge.onClose() + } + + companion object { + const val DEBOUNCE_SEARCH_DELAY = 500L + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index d54ae4ecc1..0b7cb9c397 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -5,76 +5,70 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import com.tangem.feature.swap.models.SwapSelectTokenStateHolder -import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.TokenListUMData +import com.tangem.feature.swap.models.market.state.SwapMarketState import com.tangem.feature.swap.presentation.R -import com.tangem.utils.Provider -import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.plus import kotlinx.collections.immutable.toPersistentList +@Suppress("LongParameterList") internal class TokensDataConverter( private val onSearchEntered: (String) -> Unit, private val onTokenSelected: (String) -> Unit, private val tokensDataState: CurrenciesGroup, private val isBalanceHidden: Boolean, private val isAccountsMode: Boolean, - appCurrencyProvider: Provider, -) : Transformer { + private val appCurrency: AppCurrency, + private val marketState: SwapMarketState, +) { private val accountListItemConverter = AccountTokenItemConverter( - appCurrency = appCurrencyProvider(), + appCurrency = appCurrency, unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), onItemClick = onTokenSelected, ) - override fun transform(prevState: SwapStateHolder): SwapStateHolder { + fun transform(): SwapSelectTokenStateHolder { val accountList = tokensDataState.accountCurrencyList - val currentMarketsState = prevState.selectTokenState?.marketsState - return prevState.copy( - selectTokenState = SwapSelectTokenStateHolder( - availableTokens = persistentListOf(), - unavailableTokens = persistentListOf(), - tokensListData = if (isAccountsMode) { - val portfolioList = accountListItemConverter.convertList(accountList).toPersistentList() - val totalTokensCount = portfolioList.sumOf { it.tokens.size } - if (totalTokensCount > 0) { - TokenListUMData.AccountList( - tokensList = portfolioList, - totalTokensCount = totalTokensCount, - ) - } else { - TokenListUMData.EmptyList - } + return SwapSelectTokenStateHolder( + tokensListData = if (isAccountsMode) { + val portfolioList = accountListItemConverter.convertList(accountList).toPersistentList() + val totalTokensCount = portfolioList.sumOf { it.tokens.size } + if (totalTokensCount > 0) { + TokenListUMData.AccountList( + tokensList = portfolioList, + totalTokensCount = totalTokensCount, + ) } else { - val tokensList = accountList.flatMap { (_, currencyList) -> - currencyList.asSequence().map { accountSwapCurrency -> - accountListItemConverter.createAvailableItemConverter() - .convert(accountSwapCurrency.cryptoCurrencyStatus) - }.map(TokensListItemUM::Token).toPersistentList() - }.toPersistentList() + TokenListUMData.EmptyList + } + } else { + val tokensList = accountList.flatMap { (_, currencyList) -> + currencyList.asSequence().map { accountSwapCurrency -> + accountListItemConverter.createAvailableItemConverter() + .convert(accountSwapCurrency.cryptoCurrencyStatus) + }.map(TokensListItemUM::Token).toPersistentList() + }.toPersistentList() - if (tokensList.isNotEmpty()) { - TokenListUMData.TokenList( - tokensList = persistentListOf( - TokensListItemUM.GroupTitle( - id = "available_tokens_title", - text = resourceReference(R.string.exchange_tokens_available_tokens_header), - ), - ) + tokensList, - totalTokensCount = tokensList.size, - ) - } else { - TokenListUMData.EmptyList - } - }, - marketsState = currentMarketsState, - onSearchEntered = onSearchEntered, - onTokenSelected = onTokenSelected, - isBalanceHidden = isBalanceHidden, - isAfterSearch = tokensDataState.isAfterSearch, - ), + if (tokensList.isNotEmpty()) { + TokenListUMData.TokenList( + tokensList = persistentListOf( + TokensListItemUM.GroupTitle( + id = "available_tokens_title", + text = resourceReference(R.string.exchange_tokens_available_tokens_header), + ), + ) + tokensList, + totalTokensCount = tokensList.size, + ) + } else { + TokenListUMData.EmptyList + } + }, + marketsState = marketState, + onSearchEntered = onSearchEntered, + isBalanceHidden = isBalanceHidden, + isAfterSearch = tokensDataState.isAfterSearch, ) } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 05b8f74f9a..789816698c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -10,12 +10,10 @@ import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState.InProgress.getApproveTypeOrNull -import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -38,29 +36,25 @@ import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase +import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCryptoCurrencyStatus 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.card.common.extensions.hotWalletExcludedBlockchains import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketListConfig -import com.tangem.domain.markets.toSerializableParam import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.WithdrawalResult +import com.tangem.domain.promo.ShouldShowStoriesUseCase import com.tangem.domain.promo.models.StoryContentIds import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry @@ -68,8 +62,6 @@ import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase -import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.promo.ShouldShowStoriesUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.transaction.error.GetFeeError @@ -77,8 +69,8 @@ import com.tangem.domain.transaction.models.TransactionFeeExtended import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.analytics.SwapEvents +import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter import com.tangem.feature.swap.domain.SwapInteractor @@ -89,12 +81,9 @@ import com.tangem.feature.swap.domain.models.ExpressException import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* -import com.tangem.feature.swap.models.AddToPortfolioRoute import com.tangem.feature.swap.models.SwapAlertUM import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.UiActions -import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager -import com.tangem.feature.swap.models.market.state.SwapMarketState import com.tangem.feature.swap.models.states.SwapNotificationUM import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter @@ -102,20 +91,16 @@ import com.tangem.feature.swap.ui.StateBuilder import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.features.approval.api.GiveApprovalComponent import com.tangem.features.approval.api.GiveApprovalFeatureToggles -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.features.swap.SwapComponent -import com.tangem.features.swap.SwapFeatureToggles -import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.math.RoundingMode import java.text.DecimalFormat @@ -156,15 +141,11 @@ internal class SwapModel @Inject constructor( private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase, private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork, private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, - private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, - private val swapFeatureToggles: SwapFeatureToggles, - private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, - private val excludedBlockchains: ExcludedBlockchains, - private val getUserWalletsUseCase: GetWalletsUseCase, private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase, private val appsFlyerStore: AppsFlyerStore, private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, private val messageSender: UiMessageSender, + chooseTokenBridgeFactory: ChooseTokenBridge.Factory, giveApprovalFeatureToggles: GiveApprovalFeatureToggles, ) : Model() { @@ -191,6 +172,8 @@ internal class SwapModel @Inject constructor( private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + val chooseTokenBridge: ChooseTokenBridge = chooseTokenBridgeFactory.create(modelScope) + private val stateBuilder = StateBuilder( userWalletProvider = Provider { userWallet }, actions = createUiActions(), @@ -207,7 +190,6 @@ internal class SwapModel @Inject constructor( ?: error("NumberFormat is not DecimalFormat"), ) private val amountDebouncer = Debouncer() - private val searchDebouncer = Debouncer() private val singleTaskScheduler = SingleTaskScheduler>() val dataStateStateFlow = MutableStateFlow(SwapProcessDataState()) @@ -256,44 +238,13 @@ internal class SwapModel @Inject constructor( private val fromTokenBalanceJobHolder = JobHolder() private val toTokenBalanceJobHolder = JobHolder() - private val addToPortfolioJobHolder = JobHolder() private var isAmountChangedByUser: Boolean = false private var lastPermissionNotificationTokens: Pair? = null - private val searchQueryState = MutableStateFlow("") - private val visibleMarketItemIds = MutableStateFlow>(emptyList()) - private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) - private var latestMarketsState: SwapMarketState? = null - - private val defaultMarketsListManager by lazy { - SwapMarketsListBatchFlowManager( - getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, - batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, - order = TokenMarketListConfig.Order.Trending, - currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, - currentSearchText = Provider { null }, - modelScope = modelScope, - dispatchers = dispatchers, - ) - } - - private val searchMarketsListManager by lazy { - SwapMarketsListBatchFlowManager( - getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, - batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, - order = TokenMarketListConfig.Order.ByRating, - currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, - currentSearchText = Provider { searchQueryState.value }, - modelScope = modelScope, - dispatchers = dispatchers, - ) - } - val currentScreen: SwapNavScreen get() = swapRouter.currentScreen - val bottomSheetNavigation: SlotNavigation = SlotNavigation() val approvalSlotNavigation = SlotNavigation() private val shouldUseGaslessApproval: Boolean = giveApprovalFeatureToggles.isGaslessApprovalEnabled @@ -321,35 +272,30 @@ internal class SwapModel @Inject constructor( } } - val addToPortfolioCallback = object : AddToPortfolioComponent.Callback { - override fun onDismiss() = bottomSheetNavigation.dismiss() - - override fun onSuccess(addedToken: CryptoCurrency) { - modelScope.launch { - bottomSheetNavigation.dismiss() - analyticsEventHandler.send( - SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = addedToken.symbol), - ) - analyticsEventHandler.send( - SwapAnalyticsEvent.TokenSelected( - token = addedToken.symbol, - source = ScreensSources.Markets, - isSearched = searchQueryState.value.isNotEmpty(), - ), - ) - searchQueryState.value = "" - getAccountCurrencyStatusUseCase.invoke(userWalletId, addedToken) - .firstOrNull { - it.status.value is CryptoCurrencyStatus.Loaded - }?.let { (account, status) -> - applyAddedToken(status, account) - } - } - } - } - var addToPortfolioManager: AddToPortfolioManager? = null - init { + chooseTokenBridge.searchQueryState + .onEach { query -> onSearchEntered(query) } + .launchIn(modelScope) + + chooseTokenBridge.onNewTokenAdded.receiveAsFlow() + .onEach { (addedToken, isSearched) -> + applyAddedToken(addedToken, isSearched.value) + } + .launchIn(modelScope) + + chooseTokenBridge.onTokenSelected.receiveAsFlow() + .onEach { (addedToken, isSearched) -> + onTokenSelect(addedToken, isSearched.value) + } + .launchIn(modelScope) + + chooseTokenBridge.onClose.receiveAsFlow() + .onEach { + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = false)) + swapRouter.back() + } + .launchIn(modelScope) + modelScope.launch { val storyId = StoryContentIds.STORY_FIRST_TIME_SWAP.id if (shouldShowStoriesUseCase.invokeSync(storyId)) { @@ -423,32 +369,6 @@ internal class SwapModel @Inject constructor( uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) } .launchIn(modelScope) - - subscribeMarketTokens() - - modelScope.launch { - visibleMarketItemIds.mapNotNull { rawIDS -> - if (rawIDS.isNotEmpty()) { - searchMarketsListManager.getBatchKeysByItemIds(rawIDS) - } else { - null - } - }.distinctUntilChanged().collectLatest { visibleBatchKeys -> - searchMarketsListManager.loadCharts(visibleBatchKeys) - } - } - - modelScope.launch { - visibleDefaultMarketItemIds.mapNotNull { rawIds -> - if (rawIds.isNotEmpty()) { - defaultMarketsListManager.getBatchKeysByItemIds(rawIds) - } else { - null - } - }.distinctUntilChanged().collectLatest { visibleBatchKeys -> - defaultMarketsListManager.loadCharts(visibleBatchKeys) - } - } } fun onStart() { @@ -474,41 +394,6 @@ internal class SwapModel @Inject constructor( analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(hasAvailableTokens = isAnyAvailableTokens)) } - private fun subscribeMarketTokens() { - if (swapFeatureToggles.isMarketListFeatureEnabled) { - // Switch between default and search market flows - searchQueryState - .map { it.isEmpty() } - .distinctUntilChanged() - .flatMapLatest { isDefaultMode -> - if (isDefaultMode) { - visibleMarketItemIds.value = emptyList() - createDefaultMarketsFlow() - } else { - visibleDefaultMarketItemIds.value = emptyList() - createSearchMarketsFlow() - } - } - .onEach { marketsState -> - latestMarketsState = marketsState - applyMarketsState(marketsState) - } - .launchIn(modelScope) - - // Reload search markets when query changes - searchQueryState - .onEach { searchQuery -> - if (searchQuery.isNotEmpty()) { - searchMarketsListManager.reload(searchQuery) - } - } - .launchIn(modelScope) - - // Initial load of default markets - defaultMarketsListManager.reload() - } - } - @Suppress("LongMethod") private fun initTokens(isReverseFromTo: Boolean) { modelScope.launch(dispatchers.main) { @@ -583,26 +468,40 @@ internal class SwapModel @Inject constructor( } } - private fun applyAddedToken(addedToken: CryptoCurrencyStatus, addedAccount: Account.CryptoPortfolio?) { - modelScope.launch { - runCatching(dispatchers.io) { - swapInteractor.getTokensDataState(initialCurrencyFrom) - }.onSuccess { state -> - updateTokensState(state) + private suspend fun applyAddedToken(addedToken: CryptoCurrency, isSearched: Boolean) { + analyticsEventHandler.send( + SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = addedToken.symbol), + ) + analyticsEventHandler.send( + SwapAnalyticsEvent.TokenSelected( + token = addedToken.symbol, + source = ScreensSources.Markets, + isSearched = isSearched, + ), + ) + val status = getAccountCurrencyStatusUseCase.invoke(userWalletId, addedToken) + // todo swap are sure?? about status.value is CryptoCurrencyStatus.Loaded + .firstOrNull { it.status.value is CryptoCurrencyStatus.Loaded } + ?: return + val (selectedAccount, selectedCurrency) = status - applyInitialTokenChoice( - state = state, - selectedCurrency = addedToken, - selectedAccount = addedAccount, - isReverseFromTo = isOrderReversed, - ) + runCatching(dispatchers.io) { + swapInteractor.getTokensDataState(initialCurrencyFrom) + }.onSuccess { state -> + updateTokensState(state) - subscribeToCoinBalanceUpdatesIfNeeded() + applyInitialTokenChoice( + state = state, + selectedCurrency = selectedCurrency, + selectedAccount = selectedAccount, + isReverseFromTo = isOrderReversed, + ) - swapRouter.back() - }.onFailure { error -> - TangemLogger.e("Error", error) - } + subscribeToCoinBalanceUpdatesIfNeeded() + + swapRouter.back() + }.onFailure { error -> + TangemLogger.e("Error", error) } } @@ -686,23 +585,7 @@ internal class SwapModel @Inject constructor( private fun updateTokensState(tokenDataState: TokensDataStateExpress) { val tokensDataState = if (isOrderReversed) tokenDataState.fromGroup else tokenDataState.toGroup - - uiState = stateBuilder.addTokensToStateV2( - uiState = uiState, - tokensDataState = tokensDataState, - isAccountsMode = isAccountsMode, - ) - latestMarketsState?.let(::applyMarketsState) - } - - private fun applyMarketsState(marketsState: SwapMarketState) { - uiState.selectTokenState?.let { currentSelectState -> - uiState = uiState.copy( - selectTokenState = currentSelectState.copy( - marketsState = marketsState, - ), - ) - } + chooseTokenBridge.updateCurrenciesGroup(tokensDataState) } private fun startLoadingQuotes( @@ -1274,65 +1157,61 @@ internal class SwapModel @Inject constructor( } private fun onSearchEntered(searchQuery: String) { - searchDebouncer.debounce(modelScope, DEBOUNCE_SEARCH_DELAY) { - searchQueryState.value = searchQuery - - val tokenDataState = dataState.tokensDataState ?: return@debounce - val group = if (isOrderReversed) { - tokenDataState.fromGroup - } else { - tokenDataState.toGroup - } - - val available = group.available.filter { swapAvailability -> - swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || - swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) - } - val unavailable = group.unavailable.filter { swapAvailability -> - swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || - swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) - } - val accountCurrencyList = group.accountCurrencyList.mapNotNull { accountSwapAvailability -> - val filteredCurrencies = accountSwapAvailability.currencyList.filter { accountSwapCurrency -> - val currency = accountSwapCurrency.cryptoCurrencyStatus.currency - currency.name.contains(searchQuery, ignoreCase = true) || - currency.symbol.contains(searchQuery, ignoreCase = true) - } - - if (filteredCurrencies.isEmpty()) { - return@mapNotNull null - } - - accountSwapAvailability.copy( - currencyList = filteredCurrencies, - ) - } - - val filteredTokenDataState = if (isOrderReversed) { - tokenDataState.copy( - fromGroup = tokenDataState.fromGroup.copy( - available = available, - unavailable = unavailable, - accountCurrencyList = accountCurrencyList, - isAfterSearch = true, - ), - ) - } else { - tokenDataState.copy( - toGroup = tokenDataState.toGroup.copy( - available = available, - unavailable = unavailable, - accountCurrencyList = accountCurrencyList, - isAfterSearch = true, - ), - ) - } - updateTokensState(filteredTokenDataState) + val tokenDataState = dataState.tokensDataState ?: return + val group = if (isOrderReversed) { + tokenDataState.fromGroup + } else { + tokenDataState.toGroup } + + val available = group.available.filter { swapAvailability -> + swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || + swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) + } + val unavailable = group.unavailable.filter { swapAvailability -> + swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || + swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) + } + val accountCurrencyList = group.accountCurrencyList.mapNotNull { accountSwapAvailability -> + val filteredCurrencies = accountSwapAvailability.currencyList.filter { accountSwapCurrency -> + val currency = accountSwapCurrency.cryptoCurrencyStatus.currency + currency.name.contains(searchQuery, ignoreCase = true) || + currency.symbol.contains(searchQuery, ignoreCase = true) + } + + if (filteredCurrencies.isEmpty()) { + return@mapNotNull null + } + + accountSwapAvailability.copy( + currencyList = filteredCurrencies, + ) + } + + val filteredTokenDataState = if (isOrderReversed) { + tokenDataState.copy( + fromGroup = tokenDataState.fromGroup.copy( + available = available, + unavailable = unavailable, + accountCurrencyList = accountCurrencyList, + isAfterSearch = true, + ), + ) + } else { + tokenDataState.copy( + toGroup = tokenDataState.toGroup.copy( + available = available, + unavailable = unavailable, + accountCurrencyList = accountCurrencyList, + isAfterSearch = true, + ), + ) + } + updateTokensState(filteredTokenDataState) } @Suppress("LongMethod") - private fun onTokenSelect(id: String) { + private fun onTokenSelect(id: String, isSearched: Boolean) { val tokens = dataState.tokensDataState ?: return val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id) @@ -1345,7 +1224,7 @@ internal class SwapModel @Inject constructor( SwapAnalyticsEvent.TokenSelected( token = symbol, source = ScreensSources.Portfolio, - isSearched = searchQueryState.value.isNotEmpty(), + isSearched = isSearched, ), ) } @@ -1709,8 +1588,6 @@ internal class SwapModel @Inject constructor( @Suppress("LongMethod", "CyclomaticComplexMethod") private fun createUiActions(): UiActions { return UiActions( - onSearchEntered = { onSearchEntered(it) }, - onTokenSelected = { onTokenSelect(it) }, onAmountChanged = { onAmountChanged(it) }, onSwapClick = { onSwapClick() @@ -1738,9 +1615,6 @@ internal class SwapModel @Inject constructor( if (bottomSheet != null && bottomSheet.isShown) { uiState = stateBuilder.dismissBottomSheet(uiState) } else { - if (swapRouter.currentScreen == SwapNavScreen.SelectToken) { - analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = false)) - } swapRouter.back() } onSearchEntered("") @@ -2180,110 +2054,6 @@ internal class SwapModel @Inject constructor( } } - private fun createDefaultMarketsFlow(): Flow { - val marketsTitle = TextReference.Res(R.string.feed_trending_now) - return combine( - defaultMarketsListManager.uiItems, - defaultMarketsListManager.isInInitialLoadingErrorState, - defaultMarketsListManager.totalCount, - ) { uiItems, isError, total -> - when { - isError -> SwapMarketState.LoadingError( - onRetryClicked = { defaultMarketsListManager.reload() }, - marketsTitle = marketsTitle, - shouldAssetsCount = false, - ) - uiItems.isEmpty() -> SwapMarketState.Loading( - marketsTitle = marketsTitle, - shouldAssetsCount = false, - ) - else -> SwapMarketState.Content( - items = uiItems, - loadMore = { defaultMarketsListManager.loadMore() }, - onItemClick = { item -> addToPortfolioItem(item) }, - visibleIdsChanged = { visibleDefaultMarketItemIds.value = it }, - total = total ?: uiItems.size, - marketsTitle = marketsTitle, - shouldAssetsCount = false, - ) - } - } - } - - private fun createSearchMarketsFlow(): Flow { - val marketsTitle = TextReference.Res(R.string.markets_common_title) - return combine( - flow = searchMarketsListManager.uiItems, - flow2 = searchMarketsListManager.isInInitialLoadingErrorState, - flow3 = searchMarketsListManager.isSearchNotFoundState, - flow4 = searchMarketsListManager.totalCount, - ) { uiItems, isError, isSearchNotFound, total -> - when { - isError -> SwapMarketState.LoadingError( - onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value) }, - marketsTitle = marketsTitle, - shouldAssetsCount = true, - ) - isSearchNotFound -> SwapMarketState.SearchNothingFound - uiItems.isEmpty() -> SwapMarketState.Loading( - marketsTitle = marketsTitle, - shouldAssetsCount = true, - ) - else -> SwapMarketState.Content( - items = uiItems, - loadMore = { searchMarketsListManager.loadMore() }, - onItemClick = { item -> addToPortfolioItem(item) }, - visibleIdsChanged = { visibleMarketItemIds.value = it }, - total = total ?: uiItems.size, - marketsTitle = marketsTitle, - shouldAssetsCount = true, - ) - } - } - } - - private fun addToPortfolioItem(item: MarketsListItemUM) { - modelScope.launch { - val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) - ?: searchMarketsListManager.getTokenMarketById(item.id) - ?: return@launch - - val param = tokenMarket.toSerializableParam() - val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot } - - val networks = tokenMarket.networks?.filter { network -> - BlockchainUtils.isSupportedNetworkId( - blockchainId = network.networkId, - coinId = tokenMarket.id.value, - contractAddress = network.contractAddress, - excludedBlockchains = excludedBlockchains, - hotExcludedBlockchains = hotWalletExcludedBlockchains, - hasOnlyHotWallets = hasOnlyHotWallets, - ) - }?.map { network -> - TokenMarketInfo.Network( - networkId = network.networkId, - isExchangeable = false, - contractAddress = network.contractAddress, - decimalCount = network.decimalCount, - ) - }.orEmpty() - - addToPortfolioManager = addToPortfolioManagerFactory - .create( - scope = modelScope, - token = param, - analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value), - ).apply { - setTokenNetworks(networks) - } - - addToPortfolioManager?.state - ?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd } - ?.run { bottomSheetNavigation.activate(AddToPortfolioRoute) } - }.saveIn(addToPortfolioJobHolder) - } - private fun CryptoCurrency.getNetworkInfo(): NetworkInfo { return NetworkInfo( name = this.network.name, @@ -2480,7 +2250,6 @@ internal class SwapModel @Inject constructor( const val INITIAL_AMOUNT = "" const val UPDATE_DELAY = 10000L const val DEBOUNCE_AMOUNT_DELAY = 1000L - const val DEBOUNCE_SEARCH_DELAY = 500L const val UPDATE_BALANCE_DELAY_MILLIS = 11000L const val SWAP_IN_PROGRESS_DELAY = 200L const val CHANGELLY_PROVIDER_ID = "changelly" diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index 96d1358d0a..950ef764ad 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -1,43 +1,20 @@ package com.tangem.feature.swap.models -import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM -import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.swap.models.market.state.SwapMarketState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf internal data class SwapSelectTokenStateHolder( - val availableTokens: ImmutableList, - val unavailableTokens: ImmutableList, - val marketsState: SwapMarketState? = null, + val marketsState: SwapMarketState, val tokensListData: TokenListUMData, val isBalanceHidden: Boolean, val isAfterSearch: Boolean, val onSearchEntered: (String) -> Unit, - val onTokenSelected: (String) -> Unit, -) - -internal sealed class TokenToSelectState { - - data class Title(val title: TextReference) : TokenToSelectState() - - data class TokenToSelect( - val id: String, - val name: String, - val symbol: String, - val tokenIcon: CurrencyIconState, - val isAvailable: Boolean = true, - val addedTokenBalanceData: TokenBalanceData? = null, - ) : TokenToSelectState() -} - -internal data class TokenBalanceData( - val amount: String?, - val amountEquivalent: String?, - val isBalanceHidden: Boolean, ) +@Immutable internal sealed interface TokenListUMData { val tokensList: ImmutableList @@ -64,11 +41,11 @@ internal sealed interface TokenListUMData { } internal val SwapSelectTokenStateHolder.isNotFoundState: Boolean - get() = availableTokens.isEmpty() && unavailableTokens.isEmpty() && + get() = tokensListData.tokensList.isEmpty() && isAfterSearch && - marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading + marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading internal val SwapSelectTokenStateHolder.isEmptyState: Boolean - get() = availableTokens.isEmpty() && unavailableTokens.isEmpty() && + get() = tokensListData.tokensList.isEmpty() && !isAfterSearch && - marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading \ No newline at end of file + marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index dbf80b6efc..f3d9a7bc7d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -28,7 +28,6 @@ internal data class SwapStateHolder( val priceImpact: PriceImpact, val successState: SwapSuccessStateHolder? = null, - val selectTokenState: SwapSelectTokenStateHolder? = null, val bottomSheetConfig: TangemBottomSheetConfig? = null, val swapButton: SwapButton, val shouldShowMaxAmount: Boolean, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 895a4de52c..f6bddc0a70 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -7,8 +7,6 @@ import com.tangem.feature.swap.domain.models.ui.TxFee import java.math.BigDecimal data class UiActions( - val onSearchEntered: (String) -> Unit, - val onTokenSelected: (String) -> Unit, val onAmountChanged: (String) -> Unit, val onAmountSelected: (Boolean) -> Unit, val onSwapClick: () -> Unit, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/MarketsListBatchFlowManager.kt similarity index 88% rename from features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt rename to features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/MarketsListBatchFlowManager.kt index 77609fade7..64d320b24a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/MarketsListBatchFlowManager.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.models.market import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.* import com.tangem.domain.models.currency.CryptoCurrency @@ -12,6 +13,9 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -19,14 +23,14 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @Suppress("LongParameterList") -internal class SwapMarketsListBatchFlowManager( +internal class MarketsListBatchFlowManager @AssistedInject constructor( getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, - private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, - private val order: TokenMarketListConfig.Order, - private val currentAppCurrency: Provider, - private val currentSearchText: Provider, - private val modelScope: CoroutineScope, + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val dispatchers: CoroutineDispatcherProvider, + @Assisted private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, + @Assisted private val order: TokenMarketListConfig.Order, + @Assisted private val currentSearchText: Provider, + @Assisted private val modelScope: CoroutineScope, ) { private val actionsFlow = MutableSharedFlow>(replay = 1) @@ -43,6 +47,9 @@ internal class SwapMarketsListBatchFlowManager( private val resultBatches = MutableStateFlow(ResultBatches()) private val uiBatches = resultBatches.map { it.uiBatches } + private val appCurrency: StateFlow = getSelectedAppCurrencyUseCase.invokeOrDefault() + .stateIn(modelScope, SharingStarted.Eagerly, AppCurrency.Default) + val uiItems: StateFlow> get() = uiBatches .map { batches -> @@ -114,7 +121,7 @@ internal class SwapMarketsListBatchFlowManager( val items = resultBatches.uiBatches val previousList = resultBatches.processedItems - val converter = SwapMarketsTokenItemConverter(appCurrency = currentAppCurrency()) + val converter = SwapMarketsTokenItemConverter(appCurrency = appCurrency.value) if (newList.isEmpty()) { return@update ResultBatches(processedItems = emptyList()) @@ -178,7 +185,7 @@ internal class SwapMarketsListBatchFlowManager( actionsFlow.emit( BatchAction.Reload( requestParams = TokenMarketListConfig( - fiatPriceCurrency = currentAppCurrency().code, + fiatPriceCurrency = appCurrency.value.code, searchText = if (currentSearchText() == null) { null } else { @@ -220,7 +227,7 @@ internal class SwapMarketsListBatchFlowManager( keys = batchesKeysToLoad, updateRequest = TokenMarketUpdateRequest.UpdateChart( interval = TokenMarketListConfig.Interval.H24, - currency = currentAppCurrency().code, + currency = appCurrency.value.code, ), async = true, operationId = batchesKeysToLoad.toString() + "h24", @@ -250,4 +257,14 @@ internal class SwapMarketsListBatchFlowManager( val uiBatches: List>> = emptyList(), val processedItems: List>>? = null, ) + + @AssistedFactory + interface Factory { + fun create( + batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, + order: TokenMarketListConfig.Order, + currentSearchText: Provider, + modelScope: CoroutineScope, + ): MarketsListBatchFlowManager + } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt index 8be842bcfd..e18aad5dd7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt @@ -2,9 +2,9 @@ package com.tangem.feature.swap.models.market.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.core.ui.R import kotlinx.collections.immutable.ImmutableList @Immutable @@ -38,4 +38,17 @@ internal sealed class SwapMarketState { override val marketsTitle: TextReference = TextReference.Res(R.string.markets_common_title) override val shouldAssetsCount: Boolean = true } + + companion object { + val DefaultLoading + get() = Loading( + marketsTitle = TextReference.Res(R.string.feed_trending_now), + shouldAssetsCount = false, + ) + val SearchLoading + get() = Loading( + marketsTitle = TextReference.Res(R.string.markets_common_title), + shouldAssetsCount = true, + ) + } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6d5e80eb4a..e65fefdf6c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -23,7 +23,6 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork -import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType @@ -658,21 +657,6 @@ internal class StateBuilder( ) } - fun addTokensToStateV2( - uiState: SwapStateHolder, - tokensDataState: CurrenciesGroup, - isAccountsMode: Boolean, - ): SwapStateHolder { - return TokensDataConverter( - onSearchEntered = actions.onSearchEntered, - onTokenSelected = actions.onTokenSelected, - appCurrencyProvider = appCurrencyProvider, - tokensDataState = tokensDataState, - isBalanceHidden = isBalanceHiddenProvider(), - isAccountsMode = isAccountsMode, - ).transform(uiState) - } - fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder { return uiState.copy( changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, @@ -757,28 +741,10 @@ internal class StateBuilder( val patchedReceiveCardData = uiState.receiveCardData.copy( isBalanceHidden = isBalanceHidden, ) - val selectTokenState = uiState.selectTokenState?.copy( - isBalanceHidden = isBalanceHidden, - availableTokens = uiState.selectTokenState.availableTokens.map { tokenState -> - when (tokenState) { - is TokenToSelectState.TokenToSelect -> { - tokenState.copy( - addedTokenBalanceData = tokenState.addedTokenBalanceData?.copy( - isBalanceHidden = isBalanceHidden, - ), - ) - } - is TokenToSelectState.Title -> { - tokenState - } - } - }.toImmutableList(), - ) return uiState.copy( sendCardData = patchedSendCardData, receiveCardData = patchedReceiveCardData, - selectTokenState = selectTokenState, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 2a21dd13ec..a117b2df9c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -3,7 +3,6 @@ package com.tangem.feature.swap.ui import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -26,19 +25,13 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SpacerH12 -import com.tangem.core.ui.components.SpacerH32 -import com.tangem.core.ui.components.SpacerW2 import com.tangem.core.ui.components.appbar.ExpandableSearchView import com.tangem.core.ui.components.list.InfiniteListHandler -import com.tangem.core.ui.components.atoms.text.EllipsisText -import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.tokenlist.PortfolioListItem import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem import com.tangem.core.ui.components.tokenlist.TokenListItem @@ -51,7 +44,6 @@ import com.tangem.core.ui.test.BuyTokenScreenTestTags import com.tangem.core.ui.utils.lazyListItemPosition import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenListUMData -import com.tangem.feature.swap.models.TokenToSelectState import com.tangem.feature.swap.models.isEmptyState import com.tangem.feature.swap.models.isNotFoundState import com.tangem.feature.swap.models.market.state.SwapMarketState @@ -59,7 +51,6 @@ import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.ui.market.swapMarketsListItems import com.tangem.feature.swap.ui.preview.SwapSelectTokenPreviewProvider import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf private const val LOAD_MORE_BUFFER = 25 @@ -162,12 +153,6 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = tokensListData = state.tokensListData, isBalanceHidden = state.isBalanceHidden, ) - - tokensToSelectItems(state.availableTokens, state.onTokenSelected) - - item { SpacerH12() } - - tokensToSelectItems(state.unavailableTokens, state.onTokenSelected) } } @@ -197,20 +182,6 @@ private fun ListOfTokensWithMarkets( isBalanceHidden = state.isBalanceHidden, ) - tokensToSelectItems(state.availableTokens, state.onTokenSelected) - if (state.unavailableTokens.isNotEmpty()) { - item { SpacerH12() } - tokensToSelectItems(state.unavailableTokens, state.onTokenSelected) - } - - val hasPortfolioContent = state.tokensListData !is TokenListUMData.EmptyList || - state.availableTokens.isNotEmpty() - if (hasPortfolioContent) { - item { SpacerH32() } - } else { - item { SpacerH12() } - } - swapMarketsListItems(marketsState) } @@ -374,170 +345,25 @@ private fun LazyListScope.portfolioItem( } } -private fun LazyListScope.tokensToSelectItems( - items: ImmutableList, - onTokenClick: (String) -> Unit, -) { - itemsIndexed(items = items) { index, item -> - when (item) { - is TokenToSelectState.Title -> { - TitleHeader( - item = item, - modifier = Modifier.roundedShapeItemDecoration( - currentIndex = index, - lastIndex = items.lastIndex, - ), - ) - } - is TokenToSelectState.TokenToSelect -> { - TokenItem( - token = item, - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = items.lastIndex, - ) - .background(TangemTheme.colors.background.action), - onTokenClick = { - onTokenClick(item.id) - }, - ) - } - } - } -} - -@Composable -private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .fillMaxWidth() - .background(TangemTheme.colors.background.action), - ) { - Text( - text = item.title.resolveReference().uppercase(), - style = TangemTheme.typography.overline, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .padding( - top = TangemTheme.dimens.spacing16, - start = TangemTheme.dimens.spacing16, - ), - ) - } -} - -@Suppress("LongMethod") -@Composable -private fun TokenItem( - token: TokenToSelectState.TokenToSelect, - onTokenClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .fillMaxWidth() - .height(TangemTheme.dimens.size72) - .clickable( - enabled = token.isAvailable, - onClick = onTokenClick, - ) - .padding( - vertical = TangemTheme.dimens.spacing14, - horizontal = TangemTheme.dimens.spacing16, - ), - verticalAlignment = Alignment.CenterVertically, - ) { - CurrencyIcon( - state = token.tokenIcon, - shouldDisplayNetwork = true, - ) - - Column( - modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically) - .padding(start = TangemTheme.dimens.spacing12), - ) { - EllipsisText( - text = token.name, - style = TangemTheme.typography.subtitle1, - color = if (token.isAvailable) { - TangemTheme.colors.text.primary1 - } else { - TangemTheme.colors.text.tertiary - }, - ) - SpacerW2() - EllipsisText( - text = token.symbol, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - - if (token.addedTokenBalanceData != null) { - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.Center, - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing8), - ) { - Text( - text = token.addedTokenBalanceData.amountEquivalent.orEmpty().orMaskWithStars( - maskWithStars = token.addedTokenBalanceData.isBalanceHidden && - !token.addedTokenBalanceData.amountEquivalent.isNullOrEmpty(), - ), - style = TangemTheme.typography.subtitle1, - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Visible, - color = if (token.isAvailable) { - TangemTheme.colors.text.primary1 - } else { - TangemTheme.colors.text.tertiary - }, - ) - SpacerW2() - Text( - text = token.addedTokenBalanceData.amount.orEmpty().orMaskWithStars( - maskWithStars = token.addedTokenBalanceData.isBalanceHidden && - !token.addedTokenBalanceData.amount.isNullOrEmpty(), - ), - maxLines = 1, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } - } -} - private class SwapSelectTokenScreenPreviewProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( // Content state with tokens and markets SwapSelectTokenPreviewProvider().provideSwapSelectTokenState(), // Empty state SwapSelectTokenStateHolder( - availableTokens = persistentListOf(), - unavailableTokens = persistentListOf(), tokensListData = TokenListUMData.EmptyList, - marketsState = null, + marketsState = SwapMarketState.DefaultLoading, isAfterSearch = false, isBalanceHidden = false, onSearchEntered = {}, - onTokenSelected = {}, ), // Not found state SwapSelectTokenStateHolder( - availableTokens = persistentListOf(), - unavailableTokens = persistentListOf(), tokensListData = TokenListUMData.EmptyList, - marketsState = null, + marketsState = SwapMarketState.SearchLoading, isAfterSearch = true, isBalanceHidden = false, onSearchEntered = {}, - onTokenSelected = {}, ), ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt index 8c03c69176..632255291e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt @@ -2,16 +2,13 @@ package com.tangem.feature.swap.ui.preview import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.markets.models.MarketsListItemUM -import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.core.ui.R import com.tangem.feature.swap.models.SwapSelectTokenStateHolder -import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenListUMData -import com.tangem.feature.swap.models.TokenToSelectState import com.tangem.feature.swap.models.market.state.SwapMarketState import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.persistentListOf @@ -20,13 +17,10 @@ internal class SwapSelectTokenPreviewProvider { fun provideSwapSelectTokenState(): SwapSelectTokenStateHolder { return SwapSelectTokenStateHolder( - availableTokens = listOf(previewTitle, previewToken, previewToken, previewToken).toImmutableList(), - unavailableTokens = listOf(previewTitle, previewToken, previewToken, previewToken).toImmutableList(), tokensListData = TokenListUMData.EmptyList, isAfterSearch = false, isBalanceHidden = false, onSearchEntered = {}, - onTokenSelected = {}, marketsState = createPreviewMarketsState(), ) } @@ -136,26 +130,5 @@ internal class SwapSelectTokenPreviewProvider { CHART_VALUE_6, ), ) - - private val previewToken = TokenToSelectState.TokenToSelect( - tokenIcon = CurrencyIconState.CoinIcon( - url = "", - fallbackResId = 0, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - id = "", - name = "Optimistic Ethereum (ETH)", - symbol = "USDC", - addedTokenBalanceData = TokenBalanceData( - amount = "15 000 $", - amountEquivalent = "15 000 USDT", - isBalanceHidden = false, - ), - ) - - private val previewTitle = TokenToSelectState.Title( - title = stringReference("MY TOKENS"), - ) } } \ No newline at end of file