Updated on 2026-08-14
This commit is contained in:
parent
8891721b89
commit
916a4a7242
47 changed files with 8 additions and 2802 deletions
|
|
@ -4,7 +4,6 @@ import com.tangem.domain.swap.SwapErrorResolver
|
|||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.SwapTransactionRepository
|
||||
import com.tangem.domain.swap.usecase.*
|
||||
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -19,12 +18,6 @@ import com.tangem.feature.swap.domain.api.SwapRepository as OldSwapRepository
|
|||
@InstallIn(SingletonComponent::class)
|
||||
internal object SwapDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetAvailablePairsUseCase(swapRepository: OldSwapRepository): GetAvailablePairsUseCase {
|
||||
return GetAvailablePairsUseCase(swapRepository = swapRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetSwapSupportedPairsUseCase(
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ internal class ChildFactory @Inject constructor(
|
|||
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
|
||||
private val buyCryptoComponentFactory: BuyCryptoComponent.Factory,
|
||||
private val sellCryptoComponentFactory: SellCryptoComponent.Factory,
|
||||
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
|
||||
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
|
||||
private val newWelcomeComponentFactory: NewWelcomeComponent.Factory,
|
||||
private val storiesComponentFactory: StoriesComponent.Factory,
|
||||
|
|
@ -253,13 +252,6 @@ internal class ChildFactory @Inject constructor(
|
|||
componentFactory = sellCryptoComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.SwapCrypto -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = SwapSelectTokensComponent.Params(userWalletId = route.userWalletId),
|
||||
componentFactory = swapSelectTokensComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.Onboarding -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
|
|
|
|||
|
|
@ -331,11 +331,6 @@ sealed class AppRoute(val path: String) : Route {
|
|||
val userWalletId: UserWalletId,
|
||||
) : AppRoute(path = "/sell_crypto/${userWalletId.stringValue}")
|
||||
|
||||
@Serializable
|
||||
data class SwapCrypto(
|
||||
val userWalletId: UserWalletId,
|
||||
) : AppRoute(path = "/swap_crypto/${userWalletId.stringValue}")
|
||||
|
||||
/**
|
||||
* Onboarding V2
|
||||
* @property scanResponse scan response, determines onboarding route by the product type
|
||||
|
|
|
|||
|
|
@ -82,8 +82,6 @@ sealed class MainScreenAnalyticsEvent(
|
|||
|
||||
class BuyScreenOpened : MainScreenAnalyticsEvent(event = "Buy Screen Opened")
|
||||
|
||||
class SwapScreenOpened : MainScreenAnalyticsEvent(event = "Swap Screen Opened")
|
||||
|
||||
class SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened")
|
||||
|
||||
data class BuyTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
|
|
@ -96,21 +94,6 @@ sealed class MainScreenAnalyticsEvent(
|
|||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
||||
)
|
||||
|
||||
data class SwapTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
event = "Swap Token Clicked",
|
||||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
||||
)
|
||||
|
||||
data class ReceiveTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
event = "Receive Token Clicked",
|
||||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
||||
)
|
||||
|
||||
data class RemoveTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
event = "Remove Button Clicked",
|
||||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
||||
)
|
||||
|
||||
data class ButtonClose(val source: AnalyticsParam.ScreensSources) : MainScreenAnalyticsEvent(
|
||||
event = "Button - Close",
|
||||
params = mapOf(AnalyticsParam.SOURCE to source.value),
|
||||
|
|
|
|||
|
|
@ -15,19 +15,6 @@ sealed class SwapAnalyticsEvent(
|
|||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent("Swap", event, params) {
|
||||
|
||||
data class TokenSelected(
|
||||
val token: String,
|
||||
val source: ScreensSources,
|
||||
val isSearched: Boolean,
|
||||
) : SwapAnalyticsEvent(
|
||||
event = "Token Selected",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
SOURCE to source.value,
|
||||
SEARCHED to if (isSearched) "True" else "False",
|
||||
),
|
||||
)
|
||||
|
||||
class FilterProvider(filterType: String) : SwapAnalyticsEvent(
|
||||
event = "Filter Provider",
|
||||
params = mapOf(TYPE to filterType),
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
package com.tangem.features.onramp.component
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Swap select tokens component
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface SwapSelectTokensComponent : ComposableContentComponent {
|
||||
|
||||
interface Factory : ComponentFactory<Params, SwapSelectTokensComponent>
|
||||
|
||||
/**
|
||||
* Params
|
||||
*
|
||||
* @property userWalletId user wallet id
|
||||
*/
|
||||
data class Params(val userWalletId: UserWalletId)
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
package com.tangem.features.onramp.swap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
|
||||
import com.tangem.features.onramp.component.SwapSelectTokensComponent
|
||||
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
|
||||
import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute
|
||||
import com.tangem.features.onramp.swap.model.SwapSelectTokensModel
|
||||
import com.tangem.features.onramp.swap.ui.SwapSelectTokens
|
||||
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
|
||||
import com.tangem.features.onramp.tokenlist.entity.OnrampOperation
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
@Stable
|
||||
internal class DefaultSwapSelectTokensComponent @AssistedInject constructor(
|
||||
tokenListComponentFactory: OnrampTokenListComponent.Factory,
|
||||
availableSwapPairsComponentFactory: AvailableSwapPairsComponent.Factory,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
|
||||
@Assisted private val appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SwapSelectTokensComponent.Params,
|
||||
) : AppComponentContext by appComponentContext, SwapSelectTokensComponent {
|
||||
|
||||
private val model: SwapSelectTokensModel = getOrCreateModel(params)
|
||||
|
||||
private val selectFromTokenListComponent: OnrampTokenListComponent = tokenListComponentFactory.create(
|
||||
context = child(key = "select_from_token_list"),
|
||||
params = OnrampTokenListComponent.Params(
|
||||
filterOperation = OnrampOperation.SWAP,
|
||||
userWalletId = params.userWalletId,
|
||||
onTokenClick = model::selectFromToken,
|
||||
),
|
||||
)
|
||||
|
||||
private val selectToTokenListComponent: AvailableSwapPairsComponent = availableSwapPairsComponentFactory.create(
|
||||
context = child(key = "select_to_token_list"),
|
||||
params = AvailableSwapPairsComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
selectedStatus = model.fromCurrencyStatus,
|
||||
onTokenClick = model::selectToToken,
|
||||
),
|
||||
)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = selectToTokenListComponent.bottomSheetNavigation,
|
||||
serializer = AddToPortfolioRoute.serializer(),
|
||||
key = "add_to_portfolio_bottom_sheet",
|
||||
handleBackButton = false,
|
||||
childFactory = { _, context -> bottomSheetChild(context) },
|
||||
)
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(event = MainScreenAnalyticsEvent.SwapScreenOpened())
|
||||
}
|
||||
|
||||
@Suppress("UnsafeCallOnNullableType")
|
||||
private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent {
|
||||
return addToPortfolioComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AddToPortfolioComponent.Params(
|
||||
addToPortfolioManager = selectToTokenListComponent.addToPortfolioManager,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val fromTokensState by selectFromTokenListComponent.uiState.collectAsStateWithLifecycle()
|
||||
val toTokensState by selectToTokenListComponent.uiState.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
SwapSelectTokens(
|
||||
state = state,
|
||||
selectFromTokenListComponent = selectFromTokenListComponent,
|
||||
selectFromTokenListState = fromTokensState,
|
||||
selectToTokenListComponent = selectToTokenListComponent,
|
||||
selectToTokenListState = toTokensState,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SwapSelectTokensComponent.Factory {
|
||||
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: SwapSelectTokensComponent.Params,
|
||||
): DefaultSwapSelectTokensComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.decompose.ComposableListContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
|
||||
import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** Token list component that present list of available tokens for swap */
|
||||
@Stable
|
||||
internal interface AvailableSwapPairsComponent : ComposableListContentComponent<TokenListUM> {
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute>
|
||||
val addToPortfolioManager: AddToPortfolioManager
|
||||
|
||||
/** Component factory */
|
||||
interface Factory : ComponentFactory<Params, AvailableSwapPairsComponent>
|
||||
|
||||
/**
|
||||
* Params
|
||||
*
|
||||
* @property userWalletId id of multi-currency wallet
|
||||
* @property selectedStatus flow of selected status
|
||||
* @property onTokenClick callback for token click
|
||||
*/
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val selectedStatus: StateFlow<CryptoCurrencyStatus?>,
|
||||
val onTokenClick: (TokenItemState, CryptoCurrencyStatus) -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
|
||||
import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute
|
||||
import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.ui.onrampSwapTokenList
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Stable
|
||||
internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted params: AvailableSwapPairsComponent.Params,
|
||||
) : AvailableSwapPairsComponent, AppComponentContext by context {
|
||||
|
||||
private val model: AvailableSwapPairsModel = getOrCreateModel(params)
|
||||
|
||||
override val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute> get() = model.bottomSheetNavigation
|
||||
override val addToPortfolioManager: AddToPortfolioManager get() = model.addToPortfolioManager
|
||||
|
||||
override val uiState: StateFlow<TokenListUM>
|
||||
get() = model.state
|
||||
|
||||
override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) {
|
||||
onrampSwapTokenList(state = uiState)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : AvailableSwapPairsComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: AvailableSwapPairsComponent.Params,
|
||||
): DefaultAvailableSwapPairsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.di
|
||||
|
||||
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
|
||||
import com.tangem.features.onramp.swap.availablepairs.DefaultAvailableSwapPairsComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface AvailableSwapPairsComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindAvailableSwapPairsComponentFactory(
|
||||
factory: DefaultAvailableSwapPairsComponent.Factory,
|
||||
): AvailableSwapPairsComponent.Factory
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface AvailableSwapPairsModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(AvailableSwapPairsModel::class)
|
||||
fun bindAvailableSwapPairsModel(model: AvailableSwapPairsModel): Model
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.entity.transformers
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.feature.swap.domain.models.ExpressException
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class SetErrorWarningTransformer(
|
||||
private val cause: Throwable,
|
||||
private val onRefresh: () -> Unit,
|
||||
) : TokenListUMTransformer {
|
||||
|
||||
override fun transform(prevState: TokenListUM): TokenListUM {
|
||||
return prevState.copy(
|
||||
availableItems = persistentListOf(),
|
||||
unavailableItems = persistentListOf(),
|
||||
warning = NotificationUM.Warning.OnrampErrorNotification(
|
||||
errorCode = (cause as? ExpressException)?.expressDataError?.code?.toString(),
|
||||
onRefresh = onRefresh,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.entity.transformers
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Set [statuses] as loading items
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class SetLoadingTokenItemsTransformer(
|
||||
private val statuses: List<CryptoCurrencyStatus>,
|
||||
) : TokenListUMTransformer {
|
||||
|
||||
override fun transform(prevState: TokenListUM): TokenListUM {
|
||||
return prevState.copy(
|
||||
availableItems = LoadingTokenListItemConverter.convertList(
|
||||
input = statuses.map(CryptoCurrencyStatus::currency),
|
||||
).toImmutableList(),
|
||||
unavailableItems = persistentListOf(),
|
||||
warning = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.entity.transformers
|
||||
|
||||
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
|
||||
import com.tangem.common.ui.account.TokensListPortfolioItemConverter
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SetNoAvailablePairsTransformer(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val accountList: Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>>,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val isAccountsMode: Boolean,
|
||||
private val unavailableErrorText: TextReference,
|
||||
) : TokenListUMTransformer {
|
||||
private val unavailableConverter = OnrampTokenItemStateConverterFactory
|
||||
.createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText)
|
||||
|
||||
override fun transform(prevState: TokenListUM): TokenListUM {
|
||||
val totalTokensCount = accountList.values.sumOf { it.size }
|
||||
|
||||
return prevState.copy(
|
||||
availableItems = persistentListOf(),
|
||||
unavailableItems = persistentListOf(),
|
||||
tokensListData = if (isAccountsMode) {
|
||||
TokenListUMData.AccountList(
|
||||
tokensList = accountList.map { (account, cryptoCurrencies) ->
|
||||
TokensListPortfolioItemConverter(
|
||||
tokenItemUM = AccountCryptoPortfolioItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
account = account,
|
||||
onItemClick = null,
|
||||
).convert(TotalFiatBalance.Failed),
|
||||
isExpanded = true,
|
||||
isCollapsable = false,
|
||||
tokens = unavailableConverter.convertList(cryptoCurrencies)
|
||||
.map(TokensListItemUM::Token)
|
||||
.toPersistentList(),
|
||||
).convert(Unit)
|
||||
}.toPersistentList(),
|
||||
totalTokensCount = totalTokensCount,
|
||||
)
|
||||
} else {
|
||||
TokenListUMData.TokenList(
|
||||
tokensList = accountList.flatMap { (_, cryptoCurrencies) ->
|
||||
unavailableConverter.convertList(cryptoCurrencies)
|
||||
.map(TokensListItemUM::Token)
|
||||
}.toPersistentList(),
|
||||
totalTokensCount = totalTokensCount,
|
||||
)
|
||||
},
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
warning = NotificationUM.Warning.SwapNoAvailablePair,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.market
|
||||
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.onramp.swap.availablepairs.market.converter.SwapMarketsTokenItemConverter
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SwapMarketsListBatchFlowManager(
|
||||
getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
|
||||
private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType,
|
||||
private val order: TokenMarketListConfig.Order,
|
||||
private val currentAppCurrency: Provider<AppCurrency>,
|
||||
private val currentSearchText: Provider<String?>,
|
||||
private val modelScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
private val actionsFlow = MutableSharedFlow<BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>>()
|
||||
private val updateStateJob = JobHolder()
|
||||
|
||||
private val batchFlow = getMarketsTokenListFlowUseCase(
|
||||
batchingContext = TokenListBatchingContext(
|
||||
actionsFlow = actionsFlow,
|
||||
coroutineScope = modelScope,
|
||||
),
|
||||
batchFlowType = batchFlowType,
|
||||
)
|
||||
|
||||
private val resultBatches = MutableStateFlow(ResultBatches())
|
||||
private val uiBatches = resultBatches.map { it.uiBatches }
|
||||
|
||||
val uiItems: StateFlow<ImmutableList<MarketsListItemUM>>
|
||||
get() = uiBatches
|
||||
.map { batches ->
|
||||
batches.asSequence()
|
||||
.map { it.data }
|
||||
.flatten()
|
||||
.toImmutableList()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = persistentListOf(),
|
||||
)
|
||||
|
||||
val isInInitialLoadingErrorState = batchFlow.state
|
||||
.map { it.status is PaginationStatus.InitialLoadingError }
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
val isSearchNotFoundState = batchFlow.state
|
||||
.map { batchListState ->
|
||||
currentSearchText().isNullOrEmpty().not() &&
|
||||
batchListState.status is PaginationStatus.EndOfPagination &&
|
||||
batchListState.data.isEmpty()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
val totalCount: StateFlow<Int?> = batchFlow.state
|
||||
.map { it.totalCount }
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
init {
|
||||
batchFlow.state
|
||||
.map { it.data }
|
||||
.distinctUntilChanged { a, b ->
|
||||
a.size == b.size &&
|
||||
a.map { it.key } == b.map { it.key } &&
|
||||
a.map { it.data }.flatten() == b.map { it.data }.flatten()
|
||||
}
|
||||
.onEach {
|
||||
coroutineScope {
|
||||
launch {
|
||||
updateState(it)
|
||||
}.saveIn(updateStateJob)
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private suspend fun updateState(newList: List<Batch<Int, List<TokenMarket>>>, forceUpdate: Boolean = false) =
|
||||
withContext(dispatchers.default) {
|
||||
resultBatches.update { resultBatches ->
|
||||
val items = resultBatches.uiBatches
|
||||
val previousList = resultBatches.processedItems
|
||||
|
||||
val converter = SwapMarketsTokenItemConverter(appCurrency = currentAppCurrency())
|
||||
|
||||
if (newList.isEmpty()) {
|
||||
return@update ResultBatches(processedItems = emptyList())
|
||||
}
|
||||
|
||||
val isInitialLoading =
|
||||
forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key
|
||||
|
||||
val outItems = if (isInitialLoading) {
|
||||
newList.map { batch ->
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = converter.convertList(batch.data),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (previousList.size != newList.size) {
|
||||
val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet())
|
||||
val newBatches = newList.filter { keysToAdd.contains(it.key) }
|
||||
|
||||
items + newBatches.map { batch ->
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = converter.convertList(batch.data),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items.mapIndexed { batchIndex, batch ->
|
||||
val prevBatch = previousList[batchIndex]
|
||||
val newBatch = newList[batchIndex]
|
||||
if (prevBatch == newBatch) return@mapIndexed batch
|
||||
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = batch.data.mapIndexed { index, marketsListItemUM ->
|
||||
val prevItem = prevBatch.data.getOrNull(index)
|
||||
val newItem = newBatch.data.getOrNull(index)
|
||||
if (prevItem != null && newItem != null) {
|
||||
converter.update(prevItem, marketsListItemUM, newItem)
|
||||
} else {
|
||||
newItem?.let { converter.convert(it) } ?: marketsListItemUM
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentCoroutineContext().ensureActive()
|
||||
|
||||
ResultBatches(
|
||||
uiBatches = outItems,
|
||||
processedItems = newList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun reload(searchText: String? = null) {
|
||||
modelScope.launch {
|
||||
resultBatches.value = ResultBatches()
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = TokenMarketListConfig(
|
||||
fiatPriceCurrency = currentAppCurrency().code,
|
||||
searchText = if (currentSearchText() == null) {
|
||||
null
|
||||
} else {
|
||||
searchText ?: currentSearchText()
|
||||
},
|
||||
priceChangeInterval = TokenMarketListConfig.Interval.H24,
|
||||
order = order,
|
||||
shouldNetworks = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMore() {
|
||||
modelScope.launch {
|
||||
actionsFlow.emit(BatchAction.LoadMore())
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCharts(batchKeys: Set<Int>) {
|
||||
if (batchKeys.isEmpty()) return
|
||||
|
||||
modelScope.launch {
|
||||
val currentData = batchFlow.state.value.data
|
||||
val alreadyLoadedChartsBatchKeys = currentData
|
||||
.filter { batch ->
|
||||
val first = batch.data.firstOrNull() ?: return@filter false
|
||||
first.tokenCharts.h24 != null
|
||||
}
|
||||
.map { it.key }
|
||||
.toSet()
|
||||
|
||||
val batchesKeysToLoad = batchKeys.minus(alreadyLoadedChartsBatchKeys)
|
||||
|
||||
if (batchesKeysToLoad.isNotEmpty()) {
|
||||
actionsFlow.emit(
|
||||
BatchAction.UpdateBatches(
|
||||
keys = batchesKeysToLoad,
|
||||
updateRequest = TokenMarketUpdateRequest.UpdateChart(
|
||||
interval = TokenMarketListConfig.Interval.H24,
|
||||
currency = currentAppCurrency().code,
|
||||
),
|
||||
async = true,
|
||||
operationId = batchesKeysToLoad.toString() + "h24",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getTokenMarketById(id: CryptoCurrency.RawID): TokenMarket? {
|
||||
return batchFlow.state.value.data
|
||||
.asSequence()
|
||||
.flatMap { it.data }
|
||||
.firstOrNull { it.id == id }
|
||||
}
|
||||
|
||||
fun getBatchKeysByItemIds(ids: List<CryptoCurrency.RawID>): Set<Int> {
|
||||
val currentData = batchFlow.state.value.data
|
||||
|
||||
return currentData
|
||||
.filter { d -> d.data.any { ids.contains(it.id) } }
|
||||
.map { it.key }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
private data class ResultBatches(
|
||||
val uiBatches: List<Batch<Int, List<MarketsListItemUM>>> = emptyList(),
|
||||
val processedItems: List<Batch<Int, List<TokenMarket>>>? = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.market.converter
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.compact
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class SwapMarketsTokenItemConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
) : Converter<TokenMarket, MarketsListItemUM> {
|
||||
|
||||
private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(shouldFormatAxis = false)
|
||||
|
||||
override fun convert(value: TokenMarket): MarketsListItemUM {
|
||||
return MarketsListItemUM(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
currencySymbol = value.symbol,
|
||||
ratingPosition = value.marketRating?.toString(),
|
||||
marketCap = value.getMarketCap(),
|
||||
iconUrl = value.imageUrlLarge,
|
||||
price = value.getCurrentPrice(),
|
||||
trendPercentText = value.getTrendPercent(),
|
||||
trendType = value.getTrendType(),
|
||||
chartData = value.getChartData(),
|
||||
isUnder100kMarketCap = value.isUnderMarketCapLimit,
|
||||
stakingRate = value.yieldRate?.format { percent() }?.let {
|
||||
resourceReference(R.string.markets_apy_placeholder, wrappedList(it))
|
||||
},
|
||||
updateTimestamp = value.updateTimestamp,
|
||||
networks = value.networks?.map { network ->
|
||||
MarketsListItemUM.Network(
|
||||
networkId = network.networkId,
|
||||
contractAddress = network.contractAddress,
|
||||
decimalCount = network.decimalCount,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun convertList(items: List<TokenMarket>): List<MarketsListItemUM> = items.map(::convert)
|
||||
|
||||
fun update(prev: TokenMarket, prevUI: MarketsListItemUM, new: TokenMarket): MarketsListItemUM {
|
||||
require(prev.id == new.id) {
|
||||
"Ids is not the same during update TokenMarket item: previousItem[${prev.id}] != newItem[${new.id}]"
|
||||
}
|
||||
|
||||
return prevUI.copy(
|
||||
name = new.name,
|
||||
currencySymbol = new.symbol,
|
||||
ratingPosition = new.marketRating?.toString(),
|
||||
marketCap = ifChanged(prev.marketCap, new.marketCap, prevUI.marketCap) { new.getMarketCap() },
|
||||
iconUrl = new.imageUrlLarge,
|
||||
price = ifChanged(prev = prev.tokenQuotesShort, new = new.tokenQuotesShort, prevR = prevUI.price) {
|
||||
new.getCurrentPrice(prev = prev)
|
||||
},
|
||||
trendPercentText = ifChanged(
|
||||
prev.tokenQuotesShort,
|
||||
new.tokenQuotesShort,
|
||||
prevUI.trendPercentText,
|
||||
) { new.getTrendPercent() },
|
||||
trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() },
|
||||
chartData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chartData) { new.getChartData() },
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <T, R> ifChanged(prev: T, new: T, prevR: R, force: Boolean = false, change: (T) -> R): R {
|
||||
return if (force || prev != new) change(new) else prevR
|
||||
}
|
||||
|
||||
private fun TokenMarket.getMarketCap(): String? {
|
||||
val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null
|
||||
|
||||
return value.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
).compact(
|
||||
threeDigitsMethod = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price {
|
||||
val prevPrice = prev?.tokenQuotesShort?.currentPrice
|
||||
|
||||
val priceText = tokenQuotesShort.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
).price()
|
||||
}
|
||||
|
||||
val changeType = if (prevPrice != null) {
|
||||
if (tokenQuotesShort.currentPrice > prevPrice) {
|
||||
PriceChangeType.UP
|
||||
} else {
|
||||
PriceChangeType.DOWN
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return MarketsListItemUM.Price(
|
||||
text = priceText,
|
||||
annotated = tokenQuotesShort.currentPrice.toMarketsListItemPriceAnnotated(
|
||||
appCurrencyCode = appCurrency.code,
|
||||
appCurrencySymbol = appCurrency.symbol,
|
||||
),
|
||||
changeType = changeType,
|
||||
fiatPrice = tokenQuotesShort.currentPrice,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarket.getChartData(): MarketChartRawData? {
|
||||
val chart = tokenCharts.h24
|
||||
|
||||
return chart?.let { ct ->
|
||||
priceAndTimePointValuesConverter.convert(
|
||||
MarketChartData.Data(
|
||||
y = ct.priceY.toImmutableList(),
|
||||
x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
).sorted(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun TokenMarket.getTrendType(): PriceChangeType {
|
||||
val percent = tokenQuotesShort.h24ChangePercent
|
||||
val scaled = percent?.setScale(4, RoundingMode.HALF_UP)
|
||||
return when {
|
||||
scaled == null -> PriceChangeType.NEUTRAL
|
||||
scaled > BigDecimal.ZERO -> PriceChangeType.UP
|
||||
scaled < BigDecimal.ZERO -> PriceChangeType.DOWN
|
||||
else -> PriceChangeType.NEUTRAL
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarket.getTrendPercent(): String {
|
||||
val percent = tokenQuotesShort.h24ChangePercent
|
||||
return percent.format { percent() }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.market.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal sealed class SwapMarketState {
|
||||
|
||||
abstract val marketsTitle: TextReference
|
||||
abstract val shouldAssetsCount: Boolean
|
||||
|
||||
data class Content(
|
||||
val items: ImmutableList<MarketsListItemUM>,
|
||||
val total: Int,
|
||||
val loadMore: () -> Unit,
|
||||
val onItemClick: (MarketsListItemUM) -> Unit,
|
||||
val visibleIdsChanged: (List<CryptoCurrency.RawID>) -> Unit,
|
||||
override val marketsTitle: TextReference,
|
||||
override val shouldAssetsCount: Boolean,
|
||||
) : SwapMarketState()
|
||||
|
||||
data class Loading(
|
||||
override val marketsTitle: TextReference,
|
||||
override val shouldAssetsCount: Boolean,
|
||||
) : SwapMarketState()
|
||||
|
||||
data class LoadingError(
|
||||
val onRetryClicked: () -> Unit,
|
||||
override val marketsTitle: TextReference,
|
||||
override val shouldAssetsCount: Boolean,
|
||||
) : SwapMarketState()
|
||||
|
||||
data object SearchNothingFound : SwapMarketState() {
|
||||
override val marketsTitle: TextReference = TextReference.Res(R.string.markets_common_title)
|
||||
override val shouldAssetsCount: Boolean = true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.model
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data object AddToPortfolioRoute : Route
|
||||
|
|
@ -1,648 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.model
|
||||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources
|
||||
import com.tangem.core.analytics.models.event.SwapAnalyticsEvent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.fields.InputManager
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import com.tangem.domain.core.utils.lceContent
|
||||
import com.tangem.domain.core.utils.lceError
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketListConfig
|
||||
import com.tangem.domain.markets.toSerializableParam
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.filterCryptoPortfolio
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
|
||||
import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapPairLeast
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer
|
||||
import com.tangem.features.onramp.swap.availablepairs.market.SwapMarketsListBatchFlowManager
|
||||
import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState
|
||||
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
|
||||
import com.tangem.features.onramp.swap.entity.AccountCurrencyUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.transformer.SetLoadingAccountTokenListTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateAccountTokenListTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
|
||||
import com.tangem.features.onramp.utils.ClearSearchBarTransformer
|
||||
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
|
||||
import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer
|
||||
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import com.tangem.core.ui.R as CoreUiR
|
||||
|
||||
private typealias AvailablePairsState = Lce<Throwable, List<SwapPairLeast>>
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class AvailableSwapPairsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val tokenListUMController: TokenListUMController,
|
||||
private val searchManager: InputManager,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val getAvailablePairsUseCase: GetAvailablePairsUseCase,
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
|
||||
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
getWalletsUseCase: GetWalletsUseCase,
|
||||
) : Model() {
|
||||
|
||||
val state: StateFlow<TokenListUM> = tokenListUMController.state
|
||||
|
||||
private val params: AvailableSwapPairsComponent.Params = paramsContainer.require()
|
||||
private val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
|
||||
private val allUserWallets = getWalletsUseCase.invokeSync()
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute> = SlotNavigation()
|
||||
val addToPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory
|
||||
.create(
|
||||
scope = modelScope,
|
||||
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value),
|
||||
settings = AddToPortfolioManager.Settings.ChooseToken,
|
||||
)
|
||||
|
||||
private val accountListFlow = getAccountListUseCaseFlow()
|
||||
private val availablePairsByNetworkFlow = MutableStateFlow<Map<LeastTokenInfo, AvailablePairsState>>(emptyMap())
|
||||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = getSelectedAppCurrencyUseCase.invokeOrDefault()
|
||||
.stateIn(scope = modelScope, started = SharingStarted.Eagerly, initialValue = AppCurrency.Default)
|
||||
private val refreshPairsTrigger = MutableSharedFlow<Unit>()
|
||||
private val searchQueryStateForMarkets = MutableStateFlow("")
|
||||
private val visibleMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
|
||||
|
||||
private val defaultMarketsListManager by lazy {
|
||||
SwapMarketsListBatchFlowManager(
|
||||
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main,
|
||||
order = TokenMarketListConfig.Order.Trending,
|
||||
currentAppCurrency = Provider { selectedAppCurrencyFlow.value },
|
||||
currentSearchText = Provider { null },
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
private val searchMarketsListManager by lazy {
|
||||
SwapMarketsListBatchFlowManager(
|
||||
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
|
||||
order = TokenMarketListConfig.Order.ByRating,
|
||||
currentAppCurrency = Provider { selectedAppCurrencyFlow.value },
|
||||
currentSearchText = Provider { searchQueryStateForMarkets.value },
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
private val visibleDefaultMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
|
||||
|
||||
init {
|
||||
subscribeOnUpdateState()
|
||||
|
||||
initializeSearchBarCallbacks()
|
||||
subscribeOnSelectedStatusChange()
|
||||
subscribeOnAvailablePairsUpdates()
|
||||
|
||||
subscribeOnMarketsUpdates()
|
||||
subscribeOnVisibleMarketItems()
|
||||
addToPortfolioManager.onDismiss.receiveAsFlow()
|
||||
.onEach { bottomSheetNavigation.dismiss() }
|
||||
.launchIn(modelScope)
|
||||
addToPortfolioManager.onSuccessAdded.receiveAsFlow()
|
||||
.onEach { result -> onTokenAddedToPortfolio(result.addedCurrency.currency) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun getAccountListUseCaseFlow(): SharedFlow<List<AccountStatus>> {
|
||||
return singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(params.userWalletId))
|
||||
.distinctUntilChanged()
|
||||
.mapNotNull { accountStatusList ->
|
||||
accountStatusList.accountStatuses.filter {
|
||||
it is AccountStatus.CryptoPortfolio && it.tokenList !is TokenList.Empty
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1)
|
||||
}
|
||||
|
||||
private fun subscribeOnSelectedStatusChange() {
|
||||
params.selectedStatus
|
||||
.filter { it == null }
|
||||
.onEach { clearSearchState() }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun initializeSearchBarCallbacks() {
|
||||
tokenListUMController.update(
|
||||
transformer = UpdateSearchBarCallbacksTransformer(
|
||||
onQueryChange = ::onSearchQueryChange,
|
||||
onActiveChange = ::onSearchBarActiveChange,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun subscribeOnUpdateState() {
|
||||
combine(
|
||||
flow = getAccountsAndModeFlow(),
|
||||
flow2 = getAppCurrencyAndBalanceHidingFlow(),
|
||||
flow3 = params.selectedStatus,
|
||||
flow4 = searchManager.query,
|
||||
flow5 = availablePairsByNetworkFlow
|
||||
.map { it[params.selectedStatus.value?.toLeastTokenInfo()] }
|
||||
.distinctUntilChanged(),
|
||||
) { accountListAndMode, appCurrencyAndBalanceHiding, selectedStatus, query, availablePairsState ->
|
||||
val (accountList, isAccountsMode) = accountListAndMode
|
||||
availablePairsState?.fold(
|
||||
ifLoading = {
|
||||
SetLoadingAccountTokenListTransformer(
|
||||
appCurrency = appCurrencyAndBalanceHiding.first,
|
||||
accountList = accountList,
|
||||
isAccountsMode = isAccountsMode,
|
||||
)
|
||||
},
|
||||
ifContent = { pairs ->
|
||||
handleContentState(
|
||||
appCurrencyAndBalanceHiding = appCurrencyAndBalanceHiding,
|
||||
accountList = accountList,
|
||||
selectedStatus = selectedStatus,
|
||||
query = query,
|
||||
availablePairs = pairs,
|
||||
isAccountsMode = isAccountsMode,
|
||||
)
|
||||
},
|
||||
ifError = { throwable ->
|
||||
handleErrorState(
|
||||
cause = throwable,
|
||||
networkInfo = params.selectedStatus.value?.toLeastTokenInfo(),
|
||||
accountList = accountList,
|
||||
)
|
||||
},
|
||||
) ?: SetLoadingAccountTokenListTransformer(
|
||||
appCurrency = appCurrencyAndBalanceHiding.first,
|
||||
accountList = accountList,
|
||||
isAccountsMode = isAccountsMode,
|
||||
)
|
||||
}
|
||||
.onEach(tokenListUMController::update)
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun handleContentState(
|
||||
appCurrencyAndBalanceHiding: Pair<AppCurrency, Boolean>,
|
||||
accountList: List<AccountStatus>,
|
||||
selectedStatus: CryptoCurrencyStatus?,
|
||||
query: String,
|
||||
availablePairs: List<SwapPairLeast>,
|
||||
isAccountsMode: Boolean,
|
||||
): TokenListUMTransformer {
|
||||
val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding
|
||||
|
||||
val filterByQueryAccountList: Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>> = accountList
|
||||
.filterCryptoPortfolio()
|
||||
.associate { accountStatus ->
|
||||
val statuses = accountStatus.tokenList.flattenCurrencies()
|
||||
.filterNot { status ->
|
||||
status.currency.network.rawId == selectedStatus?.currency?.network?.rawId &&
|
||||
status.currency.id.contractAddress == selectedStatus.currency.id.contractAddress
|
||||
}
|
||||
.filterByQuery(query = query)
|
||||
|
||||
accountStatus.account to statuses
|
||||
}
|
||||
.filterValues { it.isNotEmpty() }
|
||||
|
||||
if (availablePairs.isEmpty()) {
|
||||
return SetNoAvailablePairsTransformer(
|
||||
appCurrency = appCurrency,
|
||||
accountList = filterByQueryAccountList,
|
||||
unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
isAccountsMode = isAccountsMode,
|
||||
)
|
||||
}
|
||||
|
||||
return if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) {
|
||||
SetNothingToFoundStateTransformer(
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
emptySearchMessageReference = resourceReference(
|
||||
id = R.string.action_buttons_swap_empty_search_message,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
UpdateAccountTokenListTransformer(
|
||||
appCurrency = appCurrency,
|
||||
onItemClick = ::onPortfolioTokenClick,
|
||||
accountList = filterByQueryAccountList.filterByAvailability(availablePairs = availablePairs),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header),
|
||||
isAccountsMode = isAccountsMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleErrorState(
|
||||
cause: Throwable,
|
||||
networkInfo: LeastTokenInfo?,
|
||||
accountList: List<AccountStatus>,
|
||||
): SetErrorWarningTransformer {
|
||||
return SetErrorWarningTransformer(
|
||||
cause = cause,
|
||||
onRefresh = {
|
||||
modelScope.launch {
|
||||
if (networkInfo != null) {
|
||||
accountList.filterCryptoPortfolio()
|
||||
.forEach { (_, currencies) ->
|
||||
updateAvailablePairs(networkInfo, currencies.flattenCurrencies())
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun subscribeOnAvailablePairsUpdates() {
|
||||
modelScope.launch {
|
||||
combine(
|
||||
params.selectedStatus.filterNotNull(),
|
||||
refreshPairsTrigger
|
||||
.onEach { availablePairsByNetworkFlow.value = emptyMap() }
|
||||
.onStart { emit(Unit) },
|
||||
) { status, _ -> status }
|
||||
.collectLatest { selectedStatus ->
|
||||
val networkInfo = selectedStatus.toLeastTokenInfo()
|
||||
|
||||
val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() == true
|
||||
if (isAlreadyLoaded) return@collectLatest
|
||||
|
||||
val accountList = accountListFlow.firstOrNull() ?: return@collectLatest
|
||||
updateAvailablePairs(
|
||||
networkInfo = networkInfo,
|
||||
statuses = accountList.filterCryptoPortfolio()
|
||||
.flatMap { accountStatus ->
|
||||
accountStatus.flattenCurrencies()
|
||||
}.toSet().toList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateAvailablePairs(networkInfo: LeastTokenInfo, statuses: List<CryptoCurrencyStatus>) {
|
||||
runSuspendCatching {
|
||||
availablePairsByNetworkFlow.update(networkInfo = networkInfo, state = lceLoading())
|
||||
|
||||
getAvailablePairsUseCase(
|
||||
userWallet = userWallet,
|
||||
initialCurrency = networkInfo,
|
||||
currencies = statuses.map(CryptoCurrencyStatus::currency),
|
||||
)
|
||||
}
|
||||
.onSuccess { pairs ->
|
||||
availablePairsByNetworkFlow.update(networkInfo = networkInfo, state = pairs.lceContent())
|
||||
}
|
||||
.onFailure { cause ->
|
||||
availablePairsByNetworkFlow.update(networkInfo = networkInfo, state = cause.lceError())
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableStateFlow<Map<LeastTokenInfo, AvailablePairsState>>.update(
|
||||
networkInfo: LeastTokenInfo,
|
||||
state: AvailablePairsState,
|
||||
) {
|
||||
update { map ->
|
||||
map.toMutableMap().apply {
|
||||
this[networkInfo] = state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAppCurrencyAndBalanceHidingFlow(): Flow<Pair<AppCurrency, Boolean>> {
|
||||
return combine(
|
||||
flow = getSelectedAppCurrencyUseCase.invokeOrDefault(),
|
||||
flow2 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
|
||||
transform = ::Pair,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAccountsAndModeFlow(): Flow<Pair<List<AccountStatus>, Boolean>> {
|
||||
return combine(
|
||||
flow = accountListFlow.distinctUntilChanged(),
|
||||
flow2 = isAccountsModeEnabledUseCase().distinctUntilChanged(),
|
||||
transform = ::Pair,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSearchQueryChange(newQuery: String) {
|
||||
if (state.value.searchBarUM.query == newQuery) return
|
||||
|
||||
modelScope.launch {
|
||||
tokenListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery))
|
||||
|
||||
searchManager.update(newQuery)
|
||||
|
||||
searchQueryStateForMarkets.value = newQuery
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchBarActiveChange(isActive: Boolean) {
|
||||
tokenListUMController.update(
|
||||
transformer = UpdateSearchBarActiveStateTransformer(
|
||||
isActive = isActive,
|
||||
placeHolder = resourceReference(id = R.string.common_search),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<CryptoCurrencyStatus>.filterByQuery(query: String): List<CryptoCurrencyStatus> {
|
||||
return filter { status ->
|
||||
status.currency.name.contains(other = query, ignoreCase = true) ||
|
||||
status.currency.symbol.contains(other = query, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>>.filterByAvailability(
|
||||
availablePairs: List<SwapPairLeast>,
|
||||
): List<AccountAvailabilityUM> {
|
||||
return map { (account, currencies) ->
|
||||
AccountAvailabilityUM(
|
||||
account = account,
|
||||
currencyList = currencies.map { status ->
|
||||
val isAvailable = availablePairs.map(SwapPairLeast::to).contains(status.toLeastTokenInfo())
|
||||
|
||||
val isAvailableToSwap = isAvailable &&
|
||||
status.value !is CryptoCurrencyStatus.MissedDerivation &&
|
||||
status.value !is CryptoCurrencyStatus.Unreachable &&
|
||||
!status.currency.isCustom
|
||||
|
||||
AccountCurrencyUM(
|
||||
cryptoCurrencyStatus = status,
|
||||
isAvailable = isAvailableToSwap,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPortfolioTokenClick(tokenItem: TokenItemState, status: CryptoCurrencyStatus) {
|
||||
analyticsEventHandler.send(
|
||||
SwapAnalyticsEvent.TokenSelected(
|
||||
token = status.currency.symbol,
|
||||
source = ScreensSources.Portfolio,
|
||||
isSearched = state.value.searchBarUM.query.isNotEmpty(),
|
||||
),
|
||||
)
|
||||
clearSearchState()
|
||||
params.onTokenClick(tokenItem, status)
|
||||
}
|
||||
|
||||
private fun clearSearchState() {
|
||||
tokenListUMController.update(
|
||||
transformer = ClearSearchBarTransformer(
|
||||
placeHolder = resourceReference(id = R.string.common_search),
|
||||
),
|
||||
)
|
||||
modelScope.launch {
|
||||
searchManager.update("")
|
||||
}
|
||||
searchQueryStateForMarkets.value = ""
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo {
|
||||
return LeastTokenInfo(
|
||||
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0",
|
||||
network = currency.network.rawId,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun subscribeOnMarketsUpdates() {
|
||||
searchQueryStateForMarkets
|
||||
.map { it.isEmpty() }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { isDefaultMode ->
|
||||
if (isDefaultMode) {
|
||||
visibleMarketItemIds.value = emptyList()
|
||||
createDefaultMarketsFlow()
|
||||
} else {
|
||||
visibleDefaultMarketItemIds.value = emptyList()
|
||||
createSearchMarketsFlow()
|
||||
}
|
||||
}
|
||||
.onEach { marketsState ->
|
||||
tokenListUMController.update { it.copy(marketsState = marketsState) }
|
||||
}
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(modelScope)
|
||||
|
||||
searchQueryStateForMarkets
|
||||
.onEach { searchQuery ->
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
searchMarketsListManager.reload(searchQuery)
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
params.selectedStatus
|
||||
.filterNotNull()
|
||||
.take(1)
|
||||
.onEach { defaultMarketsListManager.reload() }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun createDefaultMarketsFlow(): Flow<SwapMarketState> {
|
||||
val marketsTitle = TextReference.Res(CoreUiR.string.feed_trending_now)
|
||||
return combine(
|
||||
defaultMarketsListManager.uiItems,
|
||||
defaultMarketsListManager.isInInitialLoadingErrorState,
|
||||
defaultMarketsListManager.totalCount,
|
||||
) { uiItems, isError, total ->
|
||||
when {
|
||||
isError -> SwapMarketState.LoadingError(
|
||||
onRetryClicked = { defaultMarketsListManager.reload() },
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = false,
|
||||
)
|
||||
uiItems.isEmpty() -> SwapMarketState.Loading(
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = false,
|
||||
)
|
||||
else -> SwapMarketState.Content(
|
||||
items = uiItems,
|
||||
loadMore = { defaultMarketsListManager.loadMore() },
|
||||
onItemClick = { item -> addToPortfolioItem(item) },
|
||||
visibleIdsChanged = { visibleDefaultMarketItemIds.value = it },
|
||||
total = total ?: uiItems.size,
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSearchMarketsFlow(): Flow<SwapMarketState> {
|
||||
val marketsTitle = TextReference.Res(CoreUiR.string.markets_common_title)
|
||||
return combine(
|
||||
flow = searchMarketsListManager.uiItems,
|
||||
flow2 = searchMarketsListManager.isInInitialLoadingErrorState,
|
||||
flow3 = searchMarketsListManager.isSearchNotFoundState,
|
||||
flow4 = searchMarketsListManager.totalCount,
|
||||
) { uiItems, isError, isSearchNotFound, total ->
|
||||
when {
|
||||
isError -> SwapMarketState.LoadingError(
|
||||
onRetryClicked = {
|
||||
searchMarketsListManager.reload(searchQueryStateForMarkets.value)
|
||||
},
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = true,
|
||||
)
|
||||
isSearchNotFound -> SwapMarketState.SearchNothingFound
|
||||
uiItems.isEmpty() -> SwapMarketState.Loading(
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = true,
|
||||
)
|
||||
else -> SwapMarketState.Content(
|
||||
items = uiItems,
|
||||
loadMore = { searchMarketsListManager.loadMore() },
|
||||
onItemClick = { item -> addToPortfolioItem(item) },
|
||||
visibleIdsChanged = { visibleMarketItemIds.value = it },
|
||||
total = total ?: uiItems.size,
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTokenAddedToPortfolio(addedToken: CryptoCurrency) {
|
||||
modelScope.launch {
|
||||
bottomSheetNavigation.dismiss()
|
||||
analyticsEventHandler.send(
|
||||
SwapAnalyticsEvent.TokenSelected(
|
||||
token = addedToken.symbol,
|
||||
source = ScreensSources.Markets,
|
||||
isSearched = state.value.searchBarUM.query.isNotEmpty(),
|
||||
),
|
||||
)
|
||||
|
||||
clearSearchState()
|
||||
|
||||
// Trigger re-fetch of available pairs (clears cache + re-enters collectLatest)
|
||||
refreshPairsTrigger.emit(Unit)
|
||||
|
||||
// Wait for the added token status to become Loaded
|
||||
val addedTokenStatus = getAccountCurrencyStatusUseCase(params.userWalletId, addedToken)
|
||||
.firstOrNull { it.status.value is CryptoCurrencyStatus.Loaded }
|
||||
?.status
|
||||
?: return@launch
|
||||
|
||||
// Convert to TokenItemState and trigger token selection → navigates to swap
|
||||
val converter = OnrampTokenItemStateConverterFactory.createAvailableItemConverter(
|
||||
appCurrency = selectedAppCurrencyFlow.value,
|
||||
onItemClick = params.onTokenClick,
|
||||
)
|
||||
params.onTokenClick(converter.convert(addedTokenStatus), addedTokenStatus)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addToPortfolioItem(item: MarketsListItemUM) {
|
||||
val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id)
|
||||
?: searchMarketsListManager.getTokenMarketById(item.id)
|
||||
?: return
|
||||
|
||||
val param = tokenMarket.toSerializableParam()
|
||||
val hasOnlyHotWallets = allUserWallets.all { it is UserWallet.Hot }
|
||||
|
||||
val networks = tokenMarket.networks?.filter { network ->
|
||||
BlockchainUtils.isSupportedNetworkId(
|
||||
networkId = network.networkId,
|
||||
coinId = tokenMarket.id.value,
|
||||
contractAddress = network.contractAddress,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
hotExcludedBlockchains = hotWalletExcludedBlockchains,
|
||||
hasOnlyHotWallets = hasOnlyHotWallets,
|
||||
)
|
||||
}?.map { network ->
|
||||
TokenMarketInfo.Network(
|
||||
networkId = network.networkId,
|
||||
isExchangeable = false,
|
||||
contractAddress = network.contractAddress,
|
||||
decimalCount = network.decimalCount,
|
||||
)
|
||||
}.orEmpty()
|
||||
|
||||
addToPortfolioManager.setTokenNetworks(networks)
|
||||
addToPortfolioManager.setTokenParams(param)
|
||||
|
||||
bottomSheetNavigation.activate(AddToPortfolioRoute)
|
||||
}
|
||||
|
||||
private fun subscribeOnVisibleMarketItems() {
|
||||
modelScope.launch {
|
||||
visibleMarketItemIds.mapNotNull { rawIds ->
|
||||
if (rawIds.isNotEmpty()) {
|
||||
searchMarketsListManager.getBatchKeysByItemIds(rawIds)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.distinctUntilChanged().collectLatest { visibleBatchKeys ->
|
||||
searchMarketsListManager.loadCharts(visibleBatchKeys)
|
||||
}
|
||||
}
|
||||
modelScope.launch {
|
||||
visibleDefaultMarketItemIds.mapNotNull { rawIds ->
|
||||
if (rawIds.isNotEmpty()) {
|
||||
defaultMarketsListManager.getBatchKeysByItemIds(rawIds)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.distinctUntilChanged().collectLatest { visibleBatchKeys ->
|
||||
defaultMarketsListManager.loadCharts(visibleBatchKeys)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import com.tangem.common.ui.markets.MarketsListItem
|
||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState
|
||||
|
||||
private const val LOADING_PLACEHOLDERS_COUNT = 20
|
||||
|
||||
internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) {
|
||||
item(key = "markets_title") {
|
||||
val totalCount = (state as? SwapMarketState.Content)?.total
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append(state.marketsTitle.resolveReference())
|
||||
if (totalCount != null) {
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) {
|
||||
append(" $totalCount")
|
||||
}
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.padding(top = TangemTheme.dimens.spacing24, bottom = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
|
||||
when (state) {
|
||||
is SwapMarketState.Loading -> {
|
||||
items(count = LOADING_PLACEHOLDERS_COUNT, key = { "market_placeholder_$it" }) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
is SwapMarketState.LoadingError -> {
|
||||
item(key = "market_loading_error") {
|
||||
LoadingErrorItem(
|
||||
modifier = Modifier.fillParentMaxWidth(),
|
||||
onTryAgain = state.onRetryClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
SwapMarketState.SearchNothingFound -> {
|
||||
item(key = "market_not_found") {
|
||||
SearchNothingFoundText(
|
||||
modifier = Modifier.fillParentMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
is SwapMarketState.Content -> {
|
||||
itemsIndexed(
|
||||
items = state.items,
|
||||
key = { _, item -> item.getComposeKey() },
|
||||
) { index, item ->
|
||||
MarketsListItem(
|
||||
model = item,
|
||||
onClick = { state.onItemClick(item) },
|
||||
modifier = Modifier.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = state.items.lastIndex,
|
||||
backgroundColor = TangemTheme.colors.background.action,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingErrorItem(onTryAgain: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(onRetryClick = onTryAgain)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchNothingFoundText(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier.padding(TangemTheme.dimens.spacing16),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_search_token_no_result_title),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.di
|
||||
|
||||
import com.tangem.features.onramp.component.SwapSelectTokensComponent
|
||||
import com.tangem.features.onramp.swap.DefaultSwapSelectTokensComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface SwapSelectTokensComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindSwapSelectTokensComponentFactory(
|
||||
factory: DefaultSwapSelectTokensComponent.Factory,
|
||||
): SwapSelectTokensComponent.Factory
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.onramp.swap.model.SwapSelectTokensModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface SwapSelectTokensModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SwapSelectTokensModel::class)
|
||||
fun bindOnrampTokenListModel(model: SwapSelectTokensModel): Model
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.account.AccountIconUM
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/**
|
||||
* Exchange card UI model
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal sealed interface ExchangeCardUM {
|
||||
|
||||
/** Title reference */
|
||||
val titleUM: TitleUM
|
||||
|
||||
/** Remove button UI model */
|
||||
val removeButtonUM: RemoveButtonUM?
|
||||
|
||||
/**
|
||||
* Empty state
|
||||
*
|
||||
* @property titleUM title reference
|
||||
* @property subtitleReference empty token subtitle reference
|
||||
*/
|
||||
data class Empty(
|
||||
override val titleUM: TitleUM,
|
||||
val subtitleReference: TextReference,
|
||||
) : ExchangeCardUM {
|
||||
|
||||
override val removeButtonUM: RemoveButtonUM? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Filled
|
||||
*
|
||||
* @property titleUM title reference
|
||||
* @property removeButtonUM remove button UI model
|
||||
* @property tokenItemState token item state
|
||||
*/
|
||||
data class Filled(
|
||||
override val titleUM: TitleUM,
|
||||
override val removeButtonUM: RemoveButtonUM?,
|
||||
val tokenItemState: TokenItemState,
|
||||
) : ExchangeCardUM
|
||||
|
||||
data class RemoveButtonUM(val onClick: () -> Unit)
|
||||
|
||||
@Immutable
|
||||
sealed interface TitleUM {
|
||||
|
||||
data class Text(
|
||||
val title: TextReference,
|
||||
) : TitleUM
|
||||
|
||||
data class Account(
|
||||
val prefixText: TextReference,
|
||||
val name: TextReference,
|
||||
val icon: AccountIconUM.CryptoPortfolio,
|
||||
) : TitleUM
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.entity
|
||||
|
||||
import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeFrom
|
||||
import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeTo
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* [SwapSelectTokensUM] controller
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class SwapSelectTokensController @Inject constructor() {
|
||||
|
||||
val state: StateFlow<SwapSelectTokensUM>
|
||||
field = MutableStateFlow(
|
||||
value = SwapSelectTokensUM(
|
||||
onBackClick = {},
|
||||
exchangeFrom = createEmptyExchangeFrom(),
|
||||
exchangeTo = createEmptyExchangeTo(),
|
||||
isBalanceHidden = false,
|
||||
),
|
||||
)
|
||||
|
||||
fun update(transform: (SwapSelectTokensUM) -> SwapSelectTokensUM) {
|
||||
TangemLogger.d("Applying non-name transformation")
|
||||
state.update(transform)
|
||||
}
|
||||
|
||||
fun update(transformer: SwapSelectTokensUMTransformer) {
|
||||
TangemLogger.d("Applying ${transformer::class.simpleName ?: "null"}")
|
||||
state.update(transformer::transform)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.entity
|
||||
|
||||
/**
|
||||
* Swap select tokens UI model
|
||||
*
|
||||
* @property onBackClick callback is called when back button is clicked
|
||||
* @property exchangeFrom exchange "from" card UI model
|
||||
* @property exchangeTo exchange "to" card UI model
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal data class SwapSelectTokensUM(
|
||||
val onBackClick: () -> Unit,
|
||||
val exchangeFrom: ExchangeCardUM,
|
||||
val exchangeTo: ExchangeCardUM,
|
||||
val isBalanceHidden: Boolean,
|
||||
)
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.entity
|
||||
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
/**
|
||||
* Base [SwapSelectTokensUM] transformer
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal interface SwapSelectTokensUMTransformer : Transformer<SwapSelectTokensUM>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.entity.transformer
|
||||
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer
|
||||
import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeFrom
|
||||
import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeTo
|
||||
|
||||
/**
|
||||
* Transformer for removing selected "from" token
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object RemoveSelectedFromTokenTransformer : SwapSelectTokensUMTransformer {
|
||||
|
||||
override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM {
|
||||
return prevState.copy(
|
||||
exchangeFrom = createEmptyExchangeFrom(),
|
||||
exchangeTo = createEmptyExchangeTo(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.entity.transformer
|
||||
|
||||
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer
|
||||
import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeTo
|
||||
|
||||
/**
|
||||
* Transformer for removing selected "to" token
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class RemoveSelectedToTokenTransformer(
|
||||
private val onRemoveFromTokenClick: () -> Unit,
|
||||
) : SwapSelectTokensUMTransformer {
|
||||
|
||||
override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM {
|
||||
return prevState.copy(
|
||||
exchangeFrom = prevState.exchangeFrom.showRemoveButton(onClick = onRemoveFromTokenClick),
|
||||
exchangeTo = createEmptyExchangeTo(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun ExchangeCardUM.showRemoveButton(onClick: () -> Unit): ExchangeCardUM {
|
||||
return (this as? ExchangeCardUM.Filled)
|
||||
?.copy(removeButtonUM = ExchangeCardUM.RemoveButtonUM(onClick = onClick))
|
||||
?: this
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.entity.transformer
|
||||
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer
|
||||
import com.tangem.features.onramp.swap.entity.utils.toFilled
|
||||
|
||||
/**
|
||||
* Transformer for selecting "from" token
|
||||
*
|
||||
* @property selectedTokenItemState token item state
|
||||
* @property onRemoveClick callback is called when remove button is clicked
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class SelectFromTokenTransformer(
|
||||
private val selectedTokenItemState: TokenItemState,
|
||||
private val onRemoveClick: () -> Unit,
|
||||
private val account: Account.CryptoPortfolio?,
|
||||
private val isAccountsMode: Boolean,
|
||||
) : SwapSelectTokensUMTransformer {
|
||||
|
||||
override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM {
|
||||
return prevState.copy(
|
||||
exchangeFrom = prevState.exchangeFrom.toFilled(
|
||||
selectedTokenItemState = selectedTokenItemState,
|
||||
removeButtonUM = ExchangeCardUM.RemoveButtonUM(onClick = onRemoveClick),
|
||||
account = account,
|
||||
isAccountsMode = isAccountsMode,
|
||||
isFromCurrency = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.entity.transformer
|
||||
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer
|
||||
import com.tangem.features.onramp.swap.entity.utils.toFilled
|
||||
|
||||
/**
|
||||
* Transformer for selecting "to" token
|
||||
*
|
||||
* @property selectedTokenItemState token item state
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class SelectToTokenTransformer(
|
||||
private val selectedTokenItemState: TokenItemState,
|
||||
private val isAccountsMode: Boolean,
|
||||
private val account: Account.CryptoPortfolio?,
|
||||
) : SwapSelectTokensUMTransformer {
|
||||
|
||||
override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM {
|
||||
return prevState.copy(
|
||||
exchangeFrom = prevState.exchangeFrom.hideRemoveButton(),
|
||||
exchangeTo = prevState.exchangeTo.toFilled(
|
||||
selectedTokenItemState = selectedTokenItemState,
|
||||
isAccountsMode = isAccountsMode,
|
||||
account = account,
|
||||
isFromCurrency = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun ExchangeCardUM.hideRemoveButton(): ExchangeCardUM {
|
||||
return (this as? ExchangeCardUM.Filled)?.copy(removeButtonUM = null) ?: this
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.entity.utils
|
||||
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
|
||||
|
||||
/** Create empty exchange "from" card */
|
||||
internal fun createEmptyExchangeFrom(): ExchangeCardUM.Empty {
|
||||
return ExchangeCardUM.Empty(
|
||||
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)),
|
||||
subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_swap),
|
||||
)
|
||||
}
|
||||
|
||||
/** Create empty exchange "to" card */
|
||||
internal fun createEmptyExchangeTo(): ExchangeCardUM.Empty {
|
||||
return ExchangeCardUM.Empty(
|
||||
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_to_title)),
|
||||
subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_receive),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from [ExchangeCardUM] to [ExchangeCardUM.Filled]
|
||||
*
|
||||
* @param selectedTokenItemState token item state
|
||||
* @param removeButtonUM remove button UI model
|
||||
*/
|
||||
internal fun ExchangeCardUM.toFilled(
|
||||
selectedTokenItemState: TokenItemState,
|
||||
account: Account.CryptoPortfolio?,
|
||||
isAccountsMode: Boolean,
|
||||
isFromCurrency: Boolean,
|
||||
removeButtonUM: ExchangeCardUM.RemoveButtonUM? = null,
|
||||
): ExchangeCardUM.Filled {
|
||||
return ExchangeCardUM.Filled(
|
||||
titleUM = if (account != null && isAccountsMode) {
|
||||
ExchangeCardUM.TitleUM.Account(
|
||||
prefixText = if (isFromCurrency) {
|
||||
resourceReference(R.string.common_from)
|
||||
} else {
|
||||
resourceReference(R.string.common_to)
|
||||
},
|
||||
name = account.accountName.toUM().value,
|
||||
icon = CryptoPortfolioIconConverter.convert(account.icon),
|
||||
)
|
||||
} else {
|
||||
titleUM
|
||||
},
|
||||
tokenItemState = selectedTokenItemState,
|
||||
removeButtonUM = removeButtonUM,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.model
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.onramp.component.SwapSelectTokensComponent
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensController
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
|
||||
import com.tangem.features.onramp.swap.entity.transformer.RemoveSelectedFromTokenTransformer
|
||||
import com.tangem.features.onramp.swap.entity.transformer.RemoveSelectedToTokenTransformer
|
||||
import com.tangem.features.onramp.swap.entity.transformer.SelectFromTokenTransformer
|
||||
import com.tangem.features.onramp.swap.entity.transformer.SelectToTokenTransformer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SwapSelectTokensModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val controller: SwapSelectTokensController,
|
||||
private val router: Router,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
) : Model() {
|
||||
|
||||
val state: StateFlow<SwapSelectTokensUM> = controller.state
|
||||
|
||||
val fromCurrencyStatus: StateFlow<CryptoCurrencyStatus?>
|
||||
field = MutableStateFlow<CryptoCurrencyStatus?>(value = null)
|
||||
|
||||
private val _toCurrencyStatus = MutableStateFlow<CryptoCurrencyStatus?>(value = null)
|
||||
|
||||
private val params = paramsContainer.require<SwapSelectTokensComponent.Params>()
|
||||
|
||||
private var isAccountsMode: Boolean = false
|
||||
|
||||
init {
|
||||
controller.update { it.copy(onBackClick = ::onBackClick) }
|
||||
|
||||
subscribeOnAccountsMode()
|
||||
subscribeOnBalanceHidingSettings()
|
||||
}
|
||||
|
||||
/**
|
||||
* Select "from" token
|
||||
*
|
||||
* @param selectedTokenItemState selected token item state
|
||||
* @param status crypto currency status
|
||||
*/
|
||||
fun selectFromToken(selectedTokenItemState: TokenItemState, status: CryptoCurrencyStatus) {
|
||||
analyticsEventHandler.send(
|
||||
event = MainScreenAnalyticsEvent.SwapTokenClicked(currencySymbol = status.currency.symbol),
|
||||
)
|
||||
|
||||
fromCurrencyStatus.value = status
|
||||
|
||||
modelScope.launch {
|
||||
controller.update(
|
||||
transformer = SelectFromTokenTransformer(
|
||||
selectedTokenItemState = selectedTokenItemState,
|
||||
onRemoveClick = ::onRemoveFromTokenClick,
|
||||
isAccountsMode = isAccountsMode,
|
||||
account = getAccountCurrencyStatusUseCase.invokeSync(
|
||||
userWalletId = params.userWalletId,
|
||||
currency = status.currency,
|
||||
).getOrNull()?.account,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select "to" token
|
||||
*
|
||||
* @param selectedTokenItemState selected token item state
|
||||
* @param status crypto currency status
|
||||
*/
|
||||
fun selectToToken(selectedTokenItemState: TokenItemState, status: CryptoCurrencyStatus) {
|
||||
analyticsEventHandler.send(
|
||||
event = MainScreenAnalyticsEvent.ReceiveTokenClicked(currencySymbol = status.currency.symbol),
|
||||
)
|
||||
|
||||
modelScope.launch {
|
||||
_toCurrencyStatus.value = status
|
||||
|
||||
controller.update(
|
||||
transformer = SelectToTokenTransformer(
|
||||
selectedTokenItemState = selectedTokenItemState,
|
||||
isAccountsMode = isAccountsMode,
|
||||
account = getAccountCurrencyStatusUseCase.invokeSync(
|
||||
userWalletId = params.userWalletId,
|
||||
currency = status.currency,
|
||||
).getOrNull()?.account,
|
||||
),
|
||||
)
|
||||
|
||||
// require some delay to show state with selected "from" and "to" tokens
|
||||
delay(timeMillis = 500)
|
||||
|
||||
router.push(
|
||||
route = AppRoute.Swap(
|
||||
fromCryptoCurrency = requireNotNull(fromCurrencyStatus.value).currency,
|
||||
userWalletId = params.userWalletId,
|
||||
screenSource = AnalyticsParam.ScreensSources.Main.value,
|
||||
),
|
||||
onComplete = {
|
||||
modelScope.launch {
|
||||
withTimeout(timeMillis = 500) {
|
||||
// Return a state with selected only "from" token
|
||||
removeSelectedToToken()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnBalanceHidingSettings() {
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.map { it.isBalanceHidden }
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
controller.update { state -> state.copy(isBalanceHidden = it) }
|
||||
}
|
||||
.flowOn(dispatchers.mainImmediate)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnAccountsMode() {
|
||||
isAccountsModeEnabledUseCase()
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
isAccountsMode = it
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onBackClick() {
|
||||
analyticsEventHandler.send(
|
||||
event = MainScreenAnalyticsEvent.ButtonClose(source = AnalyticsParam.ScreensSources.Swap),
|
||||
)
|
||||
|
||||
router.pop()
|
||||
}
|
||||
|
||||
private fun onRemoveFromTokenClick() {
|
||||
val currencySymbol = requireNotNull(fromCurrencyStatus.value?.currency?.symbol) {
|
||||
"Token was not selected"
|
||||
}
|
||||
|
||||
analyticsEventHandler.send(
|
||||
event = MainScreenAnalyticsEvent.RemoveTokenClicked(currencySymbol = currencySymbol),
|
||||
)
|
||||
|
||||
removeSelectedFromToken()
|
||||
}
|
||||
|
||||
private fun removeSelectedFromToken() {
|
||||
fromCurrencyStatus.value = null
|
||||
|
||||
controller.update(transformer = RemoveSelectedFromTokenTransformer)
|
||||
}
|
||||
|
||||
private fun removeSelectedToToken() {
|
||||
_toCurrencyStatus.value = null
|
||||
|
||||
controller.update(
|
||||
transformer = RemoveSelectedToTokenTransformer(onRemoveFromTokenClick = ::removeSelectedFromToken),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.account.AccountLabel
|
||||
import com.tangem.core.ui.components.account.AccountIconSize
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.components.rows.NetworkTitle
|
||||
import com.tangem.core.ui.components.token.TokenItem
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.SwapSelectTokenScreenTestTags
|
||||
import com.tangem.core.ui.utils.dashedBorder
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
|
||||
|
||||
/**
|
||||
* Exchange card
|
||||
*
|
||||
* @param state state
|
||||
* @param isBalanceHidden is balance hidden
|
||||
* @param modifier modifier
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun ExchangeCard(state: ExchangeCardUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 116.dp)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.testTag(SwapSelectTokenScreenTestTags.YOU_SWAP_BLOCK),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Title(
|
||||
titleUM = state.titleUM,
|
||||
removeButtonUM = state.removeButtonUM,
|
||||
)
|
||||
|
||||
AnimatedContent(
|
||||
targetState = state,
|
||||
transitionSpec = {
|
||||
fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90))
|
||||
.togetherWith(fadeOut(animationSpec = tween(durationMillis = 90)))
|
||||
},
|
||||
label = "TokenItem's changing",
|
||||
) { animatedState ->
|
||||
when (animatedState) {
|
||||
is ExchangeCardUM.Empty -> EmptyTokenBlock(text = animatedState.subtitleReference)
|
||||
is ExchangeCardUM.Filled -> {
|
||||
TokenItem(state = animatedState.tokenItemState, isBalanceHidden = isBalanceHidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(titleUM: ExchangeCardUM.TitleUM, removeButtonUM: ExchangeCardUM.RemoveButtonUM?) {
|
||||
NetworkTitle(
|
||||
title = {
|
||||
AnimatedContent(
|
||||
titleUM,
|
||||
) { currentState ->
|
||||
when (currentState) {
|
||||
is ExchangeCardUM.TitleUM.Account -> Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = currentState.prefixText.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
AccountLabel(
|
||||
name = currentState.name,
|
||||
icon = currentState.icon,
|
||||
iconSize = AccountIconSize.ExtraSmall,
|
||||
nameStyle = TangemTheme.typography.subtitle2,
|
||||
nameColor = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
is ExchangeCardUM.TitleUM.Text -> Text(
|
||||
text = currentState.title.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
action = { RemoveButton(state = removeButtonUM) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RemoveButton(state: ExchangeCardUM.RemoveButtonUM?) {
|
||||
AnimatedVisibility(visible = state != null) {
|
||||
state ?: return@AnimatedVisibility
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.manage_tokens_remove),
|
||||
modifier = Modifier.clickable(
|
||||
indication = ripple(bounded = false),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = state.onClick,
|
||||
),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyTokenBlock(text: TextReference, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(horizontal = 12.dp, vertical = 13.dp)
|
||||
.heightIn(min = 50.dp)
|
||||
.fillMaxWidth()
|
||||
.dashedBorder(
|
||||
color = TangemTheme.colors.icon.informative,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
dashLength = 2.dp,
|
||||
gapLength = 6.dp,
|
||||
)
|
||||
.padding(vertical = 15.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = text.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier.testTag(SwapSelectTokenScreenTestTags.CHOOSE_TOKEN_TEXT),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_ExchangeCard(@PreviewParameter(ExchangeCardUMProvider::class) state: ExchangeCardUM) {
|
||||
TangemThemePreview {
|
||||
ExchangeCard(
|
||||
state = state,
|
||||
isBalanceHidden = false,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.padding(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class ExchangeCardUMProvider : PreviewParameterProvider<ExchangeCardUM> {
|
||||
|
||||
override val values: Sequence<ExchangeCardUM> = sequenceOf(
|
||||
ExchangeCardUM.Empty(
|
||||
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)),
|
||||
subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_swap),
|
||||
),
|
||||
createFilled(removeButtonUM = null),
|
||||
createFilled(removeButtonUM = ExchangeCardUM.RemoveButtonUM { }),
|
||||
)
|
||||
|
||||
private fun createFilled(removeButtonUM: ExchangeCardUM.RemoveButtonUM?): ExchangeCardUM.Filled {
|
||||
return ExchangeCardUM.Filled(
|
||||
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)),
|
||||
removeButtonUM = removeButtonUM,
|
||||
tokenItemState = TokenItemState.Content(
|
||||
id = "1",
|
||||
iconState = CurrencyIconState.Locked,
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"),
|
||||
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"),
|
||||
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
|
||||
price = "34 496,75 \$",
|
||||
priceChangePercent = "0,43 %",
|
||||
type = PriceChangeType.DOWN,
|
||||
),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
package com.tangem.features.onramp.swap.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.components.list.InfiniteListHandler
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
|
||||
import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState
|
||||
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
|
||||
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
|
||||
private const val LOAD_MORE_BUFFER = 25
|
||||
|
||||
/**
|
||||
* Swap select tokens
|
||||
*
|
||||
* @param state state
|
||||
* @param selectFromTokenListComponent select "from" token list component
|
||||
* @param selectToTokenListComponent select "to" token list component
|
||||
* @param modifier modifier
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun SwapSelectTokens(
|
||||
state: SwapSelectTokensUM,
|
||||
selectFromTokenListComponent: OnrampTokenListComponent,
|
||||
selectFromTokenListState: TokenListUM,
|
||||
selectToTokenListComponent: AvailableSwapPairsComponent,
|
||||
selectToTokenListState: TokenListUM,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BackHandler(onBack = state.onBackClick)
|
||||
|
||||
val nestedScrollConnection = rememberHideKeyboardNestedScrollConnection()
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = 8.dp),
|
||||
) {
|
||||
swapSelectTokensContent(
|
||||
state = state,
|
||||
selectFromTokenListComponent = selectFromTokenListComponent,
|
||||
selectFromTokenListState = selectFromTokenListState,
|
||||
selectToTokenListComponent = selectToTokenListComponent,
|
||||
selectToTokenListState = selectToTokenListState,
|
||||
)
|
||||
}
|
||||
|
||||
ScrollToTopEffect(state = state, lazyListState = lazyListState)
|
||||
|
||||
MarketsHandlers(
|
||||
state = state,
|
||||
selectToTokenListState = selectToTokenListState,
|
||||
lazyListState = lazyListState,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun LazyListScope.swapSelectTokensContent(
|
||||
state: SwapSelectTokensUM,
|
||||
selectFromTokenListComponent: OnrampTokenListComponent,
|
||||
selectFromTokenListState: TokenListUM,
|
||||
selectToTokenListComponent: AvailableSwapPairsComponent,
|
||||
selectToTokenListState: TokenListUM,
|
||||
) {
|
||||
stickyHeader(key = "header") {
|
||||
AppBarWithBackButton(
|
||||
onBackClick = state.onBackClick,
|
||||
text = stringResourceSafe(id = R.string.common_swap),
|
||||
iconRes = R.drawable.ic_close_24,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
)
|
||||
}
|
||||
|
||||
item(key = "exchange_from", contentType = "exchange_from") {
|
||||
ExchangeCard(
|
||||
state = state.exchangeFrom,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 8.dp, bottom = 12.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
|
||||
if (state.exchangeFrom is ExchangeCardUM.Empty) {
|
||||
with(selectFromTokenListComponent) {
|
||||
content(uiState = selectFromTokenListState, modifier = Modifier)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.exchangeFrom is ExchangeCardUM.Filled) {
|
||||
exchangeToSection(
|
||||
state = state,
|
||||
selectToTokenListComponent = selectToTokenListComponent,
|
||||
selectToTokenListState = selectToTokenListState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun LazyListScope.exchangeToSection(
|
||||
state: SwapSelectTokensUM,
|
||||
selectToTokenListComponent: AvailableSwapPairsComponent,
|
||||
selectToTokenListState: TokenListUM,
|
||||
) {
|
||||
item(key = "exchange_to", contentType = "exchange_to") {
|
||||
if (selectToTokenListState.warning != NotificationUM.Warning.SwapNoAvailablePair) {
|
||||
ExchangeCard(
|
||||
state = state.exchangeTo,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 12.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.exchangeTo is ExchangeCardUM.Empty) {
|
||||
with(selectToTokenListComponent) {
|
||||
content(uiState = selectToTokenListState, modifier = Modifier.padding(horizontal = 16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScrollToTopEffect(state: SwapSelectTokensUM, lazyListState: LazyListState) {
|
||||
LaunchedEffect(state.exchangeFrom !is ExchangeCardUM.Empty) {
|
||||
lazyListState.scrollToItem(index = 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketsHandlers(
|
||||
state: SwapSelectTokensUM,
|
||||
selectToTokenListState: TokenListUM,
|
||||
lazyListState: LazyListState,
|
||||
) {
|
||||
// Markets for "to" token list
|
||||
if (state.exchangeFrom is ExchangeCardUM.Filled && state.exchangeTo is ExchangeCardUM.Empty) {
|
||||
MarketsPaginationHandler(
|
||||
marketsState = selectToTokenListState.marketsState,
|
||||
lazyListState = lazyListState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketsPaginationHandler(marketsState: SwapMarketState?, lazyListState: LazyListState) {
|
||||
(marketsState as? SwapMarketState.Content)?.let { content ->
|
||||
VisibleItemsTracker(lazyListState = lazyListState, marketState = content)
|
||||
|
||||
InfiniteListHandler(
|
||||
listState = lazyListState,
|
||||
buffer = LOAD_MORE_BUFFER,
|
||||
triggerLoadMoreCheckOnItemsCountChange = true,
|
||||
onLoadMore = remember(content) {
|
||||
{
|
||||
content.loadMore()
|
||||
true
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapMarketState.Content) {
|
||||
val visibleItems by remember {
|
||||
derivedStateOf {
|
||||
lazyListState.layoutInfo.visibleItemsInfo
|
||||
.mapNotNull { itemInfo ->
|
||||
marketState.items.find { it.getComposeKey() == itemInfo.key }?.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(visibleItems) {
|
||||
marketState.visibleIdsChanged(visibleItems)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.swap.entity
|
||||
package com.tangem.features.onramp.tokenlist.entity
|
||||
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.features.onramp.tokenlist.entity
|
|||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
|
|
@ -13,7 +12,6 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
* @property availableItems available items (search bar, header, tokens)
|
||||
* @property unavailableItems unavailable items (header, tokens)
|
||||
* @property isBalanceHidden flag that indicates if balance should be hidden
|
||||
* @property marketsState markets list state (null when markets should not be shown)
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
|
@ -23,8 +21,7 @@ internal data class TokenListUM(
|
|||
val unavailableItems: ImmutableList<TokensListItemUM>,
|
||||
val tokensListData: TokenListUMData,
|
||||
val isBalanceHidden: Boolean,
|
||||
val warning: NotificationUM? = null,
|
||||
val marketsState: SwapMarketState? = null,
|
||||
val warning: NotificationUM? = null
|
||||
)
|
||||
|
||||
internal sealed interface TokenListUMData {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.entity.converters
|
||||
package com.tangem.features.onramp.tokenlist.entity.transformer
|
||||
|
||||
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
|
||||
import com.tangem.common.ui.account.TokensListPortfolioItemConverter
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.entity.converters
|
||||
package com.tangem.features.onramp.tokenlist.entity.transformer
|
||||
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
|
|
@ -4,8 +4,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.filterCryptoPortfolio
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingAccountTokenItemConverter
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.AccountAvailabilityUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.AccountAvailabilityUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ import com.tangem.domain.tokens.GetAssetRequirementsUseCase
|
|||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
|
||||
import com.tangem.features.onramp.swap.entity.AccountCurrencyUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.AccountAvailabilityUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.AccountCurrencyUM
|
||||
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
|
||||
import com.tangem.features.onramp.tokenlist.entity.*
|
||||
import com.tangem.features.onramp.tokenlist.entity.transformer.SetLoadingAccountTokenListTransformer
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
|
|
@ -37,26 +36,11 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.BuyTokenScreenTestTags
|
||||
import com.tangem.core.ui.utils.lazyListItemPosition
|
||||
import com.tangem.features.onramp.swap.availablepairs.ui.swapMarketsListItems
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
||||
import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* Token list for swap - automatically switches between normal and search mode with markets
|
||||
*
|
||||
* @param state state
|
||||
*
|
||||
*/
|
||||
internal fun LazyListScope.onrampSwapTokenList(state: TokenListUM) {
|
||||
if (state.marketsState != null) {
|
||||
onrampTokenListWithMarkets(state = state)
|
||||
} else {
|
||||
onrampTokenList(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token list - normal mode (without markets)
|
||||
*
|
||||
|
|
@ -74,39 +58,6 @@ internal fun LazyListScope.onrampTokenList(state: TokenListUM) {
|
|||
tokensListData(state = state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Token list with markets - search mode
|
||||
*
|
||||
* @param state state
|
||||
*/
|
||||
private fun LazyListScope.onrampTokenListWithMarkets(state: TokenListUM) {
|
||||
val itemModifier = Modifier.padding(horizontal = 16.dp)
|
||||
|
||||
warningOrSearchBar(state = state, itemModifier = itemModifier)
|
||||
|
||||
// Check if user has any assets to show
|
||||
val hasAssets = state.availableItems.isNotEmpty() ||
|
||||
state.unavailableItems.isNotEmpty() ||
|
||||
state.tokensListData.totalTokensCount != 0
|
||||
|
||||
if (hasAssets) {
|
||||
assetsTitle(
|
||||
count = state.tokensListData.totalTokensCount,
|
||||
showCount = state.marketsState?.shouldAssetsCount == true,
|
||||
)
|
||||
|
||||
tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden)
|
||||
|
||||
tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden)
|
||||
|
||||
tokensListData(state = state)
|
||||
|
||||
item { SpacerH32() }
|
||||
}
|
||||
|
||||
state.marketsState?.let(::swapMarketsListItems)
|
||||
}
|
||||
|
||||
private fun LazyListScope.warningOrSearchBar(state: TokenListUM, itemModifier: Modifier) {
|
||||
if (state.warning == null) {
|
||||
searchBarItem(searchBarUM = state.searchBarUM, modifier = itemModifier)
|
||||
|
|
|
|||
|
|
@ -49,11 +49,9 @@ import com.tangem.datasource.api.express.models.request.LeastTokenInfo as Networ
|
|||
internal class DefaultSwapRepository(
|
||||
private val tangemExpressApi: TangemExpressApi,
|
||||
private val coroutineDispatcher: CoroutineDispatcherProvider,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val errorsDataConverter: ErrorsDataConverter,
|
||||
private val dataSignatureVerifier: DataSignatureVerifier,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val rampStateManager: RampStateManager,
|
||||
private val expressHistoryDao: ExpressHistoryDao,
|
||||
moshi: Moshi,
|
||||
) : SwapRepository {
|
||||
|
|
@ -122,98 +120,6 @@ internal class DefaultSwapRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getPairsOnly(
|
||||
userWallet: UserWallet,
|
||||
initialCurrency: LeastTokenInfo,
|
||||
currencyList: List<CryptoCurrency>,
|
||||
isIgnoreExpress: Boolean,
|
||||
): PairsWithProviders {
|
||||
return withContext(coroutineDispatcher.io) {
|
||||
val currenciesList = filterByAssetRequirements(userWallet, currencyList)
|
||||
|
||||
if (isIgnoreExpress) {
|
||||
buildLocalPairs(initialCurrency, currenciesList)
|
||||
} else {
|
||||
fetchExpressPairs(userWallet, initialCurrency, currenciesList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildLocalPairs(
|
||||
initialCurrency: LeastTokenInfo,
|
||||
currenciesList: List<NetworkLeastTokenInfo>,
|
||||
): PairsWithProviders {
|
||||
val pairs = currenciesList.map { tokenInfo ->
|
||||
SwapPairLeast(
|
||||
from = initialCurrency,
|
||||
to = LeastTokenInfo(
|
||||
contractAddress = tokenInfo.contractAddress,
|
||||
network = tokenInfo.network,
|
||||
),
|
||||
providers = emptyList(),
|
||||
)
|
||||
}
|
||||
return PairsWithProviders(pairs = pairs, allProviders = emptyList())
|
||||
}
|
||||
|
||||
private suspend fun fetchExpressPairs(
|
||||
userWallet: UserWallet,
|
||||
initialCurrency: LeastTokenInfo,
|
||||
currenciesList: List<NetworkLeastTokenInfo>,
|
||||
): PairsWithProviders {
|
||||
try {
|
||||
val initial = NetworkLeastTokenInfo(
|
||||
contractAddress = initialCurrency.contractAddress,
|
||||
network = initialCurrency.network,
|
||||
)
|
||||
|
||||
val allPairs = supervisorScope {
|
||||
val pairsDeferred = async {
|
||||
getPairsInternal(
|
||||
userWallet = userWallet,
|
||||
from = arrayListOf(initial),
|
||||
to = currenciesList,
|
||||
)
|
||||
}
|
||||
|
||||
val reversedPairsDeferred = async {
|
||||
getPairsInternal(
|
||||
userWallet = userWallet,
|
||||
from = currenciesList,
|
||||
to = arrayListOf(initial),
|
||||
)
|
||||
}
|
||||
|
||||
pairsDeferred.await().getOrThrow() + reversedPairsDeferred.await().getOrThrow()
|
||||
}
|
||||
|
||||
return swapPairInfoConverter.convert(
|
||||
SwapPairsWithProviders(
|
||||
swapPair = allPairs,
|
||||
providers = emptyList(),
|
||||
),
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
if (exception is ApiResponseError.HttpException) {
|
||||
throw ExpressException(errorsDataConverter.convert(exception.errorBody.orEmpty()))
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun filterByAssetRequirements(
|
||||
userWallet: UserWallet,
|
||||
currencyList: List<CryptoCurrency>,
|
||||
): List<NetworkLeastTokenInfo> {
|
||||
return currencyList
|
||||
.filter { currency ->
|
||||
val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, currency)
|
||||
rampStateManager.checkAssetRequirements(requirements)
|
||||
}
|
||||
.map { currency -> leastTokenInfoConverter.convert(currency) }
|
||||
}
|
||||
|
||||
private suspend fun getPairsInternal(
|
||||
userWallet: UserWallet,
|
||||
from: List<NetworkLeastTokenInfo>,
|
||||
|
|
|
|||
|
|
@ -39,22 +39,18 @@ internal class SwapDataModule {
|
|||
tangemExpressApi: TangemExpressApi,
|
||||
coroutineDispatcher: CoroutineDispatcherProvider,
|
||||
dataSignature: DataSignatureVerifier,
|
||||
walletManagerFacade: WalletManagersFacade,
|
||||
errorsDataConverter: ErrorsDataConverter,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
rampStateManager: RampStateManager,
|
||||
expressHistoryDao: ExpressHistoryDao,
|
||||
): SwapRepository {
|
||||
return DefaultSwapRepository(
|
||||
tangemExpressApi = tangemExpressApi,
|
||||
coroutineDispatcher = coroutineDispatcher,
|
||||
walletManagersFacade = walletManagerFacade,
|
||||
errorsDataConverter = errorsDataConverter,
|
||||
dataSignatureVerifier = dataSignature,
|
||||
moshi = moshi,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
rampStateManager = rampStateManager,
|
||||
expressHistoryDao = expressHistoryDao,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||
import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapPairLeast
|
||||
|
||||
class GetAvailablePairsUseCase(
|
||||
private val swapRepository: SwapRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
userWallet: UserWallet,
|
||||
initialCurrency: LeastTokenInfo,
|
||||
currencies: List<CryptoCurrency>,
|
||||
): List<SwapPairLeast> {
|
||||
return swapRepository.getPairsOnly(
|
||||
userWallet = userWallet,
|
||||
initialCurrency = initialCurrency,
|
||||
currencyList = currencies,
|
||||
isIgnoreExpress = true,
|
||||
).pairs
|
||||
}
|
||||
}
|
||||
|
|
@ -16,14 +16,6 @@ interface SwapRepository {
|
|||
currencyList: List<CryptoCurrency>,
|
||||
): PairsWithProviders
|
||||
|
||||
/** Express getPairs request variant without providers request */
|
||||
suspend fun getPairsOnly(
|
||||
userWallet: UserWallet,
|
||||
initialCurrency: LeastTokenInfo,
|
||||
currencyList: List<CryptoCurrency>,
|
||||
isIgnoreExpress: Boolean = false,
|
||||
): PairsWithProviders
|
||||
|
||||
suspend fun getExchangeStatus(
|
||||
userWallet: UserWallet?,
|
||||
userWalletId: UserWalletId,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue