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.SwapRepositoryV2
|
||||||
import com.tangem.domain.swap.SwapTransactionRepository
|
import com.tangem.domain.swap.SwapTransactionRepository
|
||||||
import com.tangem.domain.swap.usecase.*
|
import com.tangem.domain.swap.usecase.*
|
||||||
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -19,12 +18,6 @@ import com.tangem.feature.swap.domain.api.SwapRepository as OldSwapRepository
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
internal object SwapDomainModule {
|
internal object SwapDomainModule {
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideGetAvailablePairsUseCase(swapRepository: OldSwapRepository): GetAvailablePairsUseCase {
|
|
||||||
return GetAvailablePairsUseCase(swapRepository = swapRepository)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideGetSwapSupportedPairsUseCase(
|
fun provideGetSwapSupportedPairsUseCase(
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,6 @@ internal class ChildFactory @Inject constructor(
|
||||||
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
|
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
|
||||||
private val buyCryptoComponentFactory: BuyCryptoComponent.Factory,
|
private val buyCryptoComponentFactory: BuyCryptoComponent.Factory,
|
||||||
private val sellCryptoComponentFactory: SellCryptoComponent.Factory,
|
private val sellCryptoComponentFactory: SellCryptoComponent.Factory,
|
||||||
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
|
|
||||||
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
|
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
|
||||||
private val newWelcomeComponentFactory: NewWelcomeComponent.Factory,
|
private val newWelcomeComponentFactory: NewWelcomeComponent.Factory,
|
||||||
private val storiesComponentFactory: StoriesComponent.Factory,
|
private val storiesComponentFactory: StoriesComponent.Factory,
|
||||||
|
|
@ -253,13 +252,6 @@ internal class ChildFactory @Inject constructor(
|
||||||
componentFactory = sellCryptoComponentFactory,
|
componentFactory = sellCryptoComponentFactory,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AppRoute.SwapCrypto -> {
|
|
||||||
createComponentChild(
|
|
||||||
context = context,
|
|
||||||
params = SwapSelectTokensComponent.Params(userWalletId = route.userWalletId),
|
|
||||||
componentFactory = swapSelectTokensComponentFactory,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is AppRoute.Onboarding -> {
|
is AppRoute.Onboarding -> {
|
||||||
createComponentChild(
|
createComponentChild(
|
||||||
context = context,
|
context = context,
|
||||||
|
|
|
||||||
|
|
@ -331,11 +331,6 @@ sealed class AppRoute(val path: String) : Route {
|
||||||
val userWalletId: UserWalletId,
|
val userWalletId: UserWalletId,
|
||||||
) : AppRoute(path = "/sell_crypto/${userWalletId.stringValue}")
|
) : AppRoute(path = "/sell_crypto/${userWalletId.stringValue}")
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SwapCrypto(
|
|
||||||
val userWalletId: UserWalletId,
|
|
||||||
) : AppRoute(path = "/swap_crypto/${userWalletId.stringValue}")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Onboarding V2
|
* Onboarding V2
|
||||||
* @property scanResponse scan response, determines onboarding route by the product type
|
* @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 BuyScreenOpened : MainScreenAnalyticsEvent(event = "Buy Screen Opened")
|
||||||
|
|
||||||
class SwapScreenOpened : MainScreenAnalyticsEvent(event = "Swap Screen Opened")
|
|
||||||
|
|
||||||
class SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened")
|
class SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened")
|
||||||
|
|
||||||
data class BuyTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
data class BuyTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||||
|
|
@ -96,21 +94,6 @@ sealed class MainScreenAnalyticsEvent(
|
||||||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
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(
|
data class ButtonClose(val source: AnalyticsParam.ScreensSources) : MainScreenAnalyticsEvent(
|
||||||
event = "Button - Close",
|
event = "Button - Close",
|
||||||
params = mapOf(AnalyticsParam.SOURCE to source.value),
|
params = mapOf(AnalyticsParam.SOURCE to source.value),
|
||||||
|
|
|
||||||
|
|
@ -15,19 +15,6 @@ sealed class SwapAnalyticsEvent(
|
||||||
params: Map<String, String> = emptyMap(),
|
params: Map<String, String> = emptyMap(),
|
||||||
) : AnalyticsEvent("Swap", event, params) {
|
) : 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(
|
class FilterProvider(filterType: String) : SwapAnalyticsEvent(
|
||||||
event = "Filter Provider",
|
event = "Filter Provider",
|
||||||
params = mapOf(TYPE to filterType),
|
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.account.Account
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
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.common.ui.notifications.NotificationUM
|
||||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||||
import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -13,7 +12,6 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
* @property availableItems available items (search bar, header, tokens)
|
* @property availableItems available items (search bar, header, tokens)
|
||||||
* @property unavailableItems unavailable items (header, tokens)
|
* @property unavailableItems unavailable items (header, tokens)
|
||||||
* @property isBalanceHidden flag that indicates if balance should be hidden
|
* @property isBalanceHidden flag that indicates if balance should be hidden
|
||||||
* @property marketsState markets list state (null when markets should not be shown)
|
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
|
|
@ -23,8 +21,7 @@ internal data class TokenListUM(
|
||||||
val unavailableItems: ImmutableList<TokensListItemUM>,
|
val unavailableItems: ImmutableList<TokensListItemUM>,
|
||||||
val tokensListData: TokenListUMData,
|
val tokensListData: TokenListUMData,
|
||||||
val isBalanceHidden: Boolean,
|
val isBalanceHidden: Boolean,
|
||||||
val warning: NotificationUM? = null,
|
val warning: NotificationUM? = null
|
||||||
val marketsState: SwapMarketState? = null,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
internal sealed interface TokenListUMData {
|
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.AccountCryptoPortfolioItemStateConverter
|
||||||
import com.tangem.common.ui.account.TokensListPortfolioItemConverter
|
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.currency.icon.CurrencyIconState
|
||||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
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.AccountStatus
|
||||||
import com.tangem.domain.models.account.filterCryptoPortfolio
|
import com.tangem.domain.models.account.filterCryptoPortfolio
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
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.TokenListUM
|
||||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
||||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
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.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.models.TotalFiatBalance
|
import com.tangem.domain.models.TotalFiatBalance
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
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.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
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.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
import com.tangem.features.onramp.impl.R
|
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.TokenListUM
|
||||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
||||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
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.tokens.model.ScenarioUnavailabilityReason
|
||||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||||
import com.tangem.features.onramp.impl.R
|
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.swap.entity.AccountCurrencyUM
|
import com.tangem.features.onramp.tokenlist.entity.AccountCurrencyUM
|
||||||
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
|
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
|
||||||
import com.tangem.features.onramp.tokenlist.entity.*
|
import com.tangem.features.onramp.tokenlist.entity.*
|
||||||
import com.tangem.features.onramp.tokenlist.entity.transformer.SetLoadingAccountTokenListTransformer
|
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 androidx.compose.ui.unit.dp
|
||||||
import com.tangem.common.ui.notifications.NotificationUM
|
import com.tangem.common.ui.notifications.NotificationUM
|
||||||
import com.tangem.core.ui.R
|
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.SearchBar
|
||||||
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
|
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
|
||||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
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.res.TangemThemePreview
|
||||||
import com.tangem.core.ui.test.BuyTokenScreenTestTags
|
import com.tangem.core.ui.test.BuyTokenScreenTestTags
|
||||||
import com.tangem.core.ui.utils.lazyListItemPosition
|
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.TokenListUM
|
||||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
||||||
import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider
|
import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
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)
|
* Token list - normal mode (without markets)
|
||||||
*
|
*
|
||||||
|
|
@ -74,39 +58,6 @@ internal fun LazyListScope.onrampTokenList(state: TokenListUM) {
|
||||||
tokensListData(state = state)
|
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) {
|
private fun LazyListScope.warningOrSearchBar(state: TokenListUM, itemModifier: Modifier) {
|
||||||
if (state.warning == null) {
|
if (state.warning == null) {
|
||||||
searchBarItem(searchBarUM = state.searchBarUM, modifier = itemModifier)
|
searchBarItem(searchBarUM = state.searchBarUM, modifier = itemModifier)
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,9 @@ import com.tangem.datasource.api.express.models.request.LeastTokenInfo as Networ
|
||||||
internal class DefaultSwapRepository(
|
internal class DefaultSwapRepository(
|
||||||
private val tangemExpressApi: TangemExpressApi,
|
private val tangemExpressApi: TangemExpressApi,
|
||||||
private val coroutineDispatcher: CoroutineDispatcherProvider,
|
private val coroutineDispatcher: CoroutineDispatcherProvider,
|
||||||
private val walletManagersFacade: WalletManagersFacade,
|
|
||||||
private val errorsDataConverter: ErrorsDataConverter,
|
private val errorsDataConverter: ErrorsDataConverter,
|
||||||
private val dataSignatureVerifier: DataSignatureVerifier,
|
private val dataSignatureVerifier: DataSignatureVerifier,
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val rampStateManager: RampStateManager,
|
|
||||||
private val expressHistoryDao: ExpressHistoryDao,
|
private val expressHistoryDao: ExpressHistoryDao,
|
||||||
moshi: Moshi,
|
moshi: Moshi,
|
||||||
) : SwapRepository {
|
) : 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(
|
private suspend fun getPairsInternal(
|
||||||
userWallet: UserWallet,
|
userWallet: UserWallet,
|
||||||
from: List<NetworkLeastTokenInfo>,
|
from: List<NetworkLeastTokenInfo>,
|
||||||
|
|
|
||||||
|
|
@ -39,22 +39,18 @@ internal class SwapDataModule {
|
||||||
tangemExpressApi: TangemExpressApi,
|
tangemExpressApi: TangemExpressApi,
|
||||||
coroutineDispatcher: CoroutineDispatcherProvider,
|
coroutineDispatcher: CoroutineDispatcherProvider,
|
||||||
dataSignature: DataSignatureVerifier,
|
dataSignature: DataSignatureVerifier,
|
||||||
walletManagerFacade: WalletManagersFacade,
|
|
||||||
errorsDataConverter: ErrorsDataConverter,
|
errorsDataConverter: ErrorsDataConverter,
|
||||||
@NetworkMoshi moshi: Moshi,
|
@NetworkMoshi moshi: Moshi,
|
||||||
appPreferencesStore: AppPreferencesStore,
|
appPreferencesStore: AppPreferencesStore,
|
||||||
rampStateManager: RampStateManager,
|
|
||||||
expressHistoryDao: ExpressHistoryDao,
|
expressHistoryDao: ExpressHistoryDao,
|
||||||
): SwapRepository {
|
): SwapRepository {
|
||||||
return DefaultSwapRepository(
|
return DefaultSwapRepository(
|
||||||
tangemExpressApi = tangemExpressApi,
|
tangemExpressApi = tangemExpressApi,
|
||||||
coroutineDispatcher = coroutineDispatcher,
|
coroutineDispatcher = coroutineDispatcher,
|
||||||
walletManagersFacade = walletManagerFacade,
|
|
||||||
errorsDataConverter = errorsDataConverter,
|
errorsDataConverter = errorsDataConverter,
|
||||||
dataSignatureVerifier = dataSignature,
|
dataSignatureVerifier = dataSignature,
|
||||||
moshi = moshi,
|
moshi = moshi,
|
||||||
appPreferencesStore = appPreferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
rampStateManager = rampStateManager,
|
|
||||||
expressHistoryDao = expressHistoryDao,
|
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>,
|
currencyList: List<CryptoCurrency>,
|
||||||
): PairsWithProviders
|
): 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(
|
suspend fun getExchangeStatus(
|
||||||
userWallet: UserWallet?,
|
userWallet: UserWallet?,
|
||||||
userWalletId: UserWalletId,
|
userWalletId: UserWalletId,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue