Updated on 2026-08-14
This commit is contained in:
parent
1d9d74b6a9
commit
fe2bade1e4
7 changed files with 161 additions and 4 deletions
|
|
@ -19,6 +19,7 @@ dependencies {
|
|||
implementation(projects.features.swap.domain)
|
||||
implementation(projects.features.swap.domain.api)
|
||||
implementation(projects.features.swap.domain.models)
|
||||
implementation(projects.features.feed.api)
|
||||
|
||||
/** Project - Core */
|
||||
implementation(projects.core.analytics)
|
||||
|
|
|
|||
|
|
@ -5,13 +5,20 @@ 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.feed.components.market.details.portfolio.add.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
|
||||
|
|
@ -25,6 +32,7 @@ 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 {
|
||||
|
|
@ -49,15 +57,36 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
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!!,
|
||||
callback = selectToTokenListComponent.addToPortfolioCallback,
|
||||
shouldSkipTokenActionsScreen = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@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,
|
||||
|
|
@ -67,6 +96,8 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor(
|
|||
selectToTokenListState = toTokensState,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
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.feed.components.market.details.portfolio.add.AddToPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager
|
||||
import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
|
|
@ -13,6 +17,10 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
@Stable
|
||||
internal interface AvailableSwapPairsComponent : ComposableListContentComponent<TokenListUM> {
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute>
|
||||
val addToPortfolioManager: AddToPortfolioManager?
|
||||
val addToPortfolioCallback: AddToPortfolioComponent.Callback
|
||||
|
||||
/** Component factory */
|
||||
interface Factory : ComponentFactory<Params, AvailableSwapPairsComponent>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,12 @@ 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.feed.components.market.details.portfolio.add.AddToPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.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
|
||||
|
|
@ -21,6 +25,10 @@ internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor(
|
|||
|
||||
private val model: AvailableSwapPairsModel = getOrCreateModel(params)
|
||||
|
||||
override val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute> get() = model.bottomSheetNavigation
|
||||
override val addToPortfolioManager: AddToPortfolioManager? get() = model.addToPortfolioManager
|
||||
override val addToPortfolioCallback: AddToPortfolioComponent.Callback get() = model.addToPortfolioCallback
|
||||
|
||||
override val uiState: StateFlow<TokenListUM>
|
||||
get() = model.state
|
||||
|
||||
|
|
|
|||
|
|
@ -228,6 +228,13 @@ internal class SwapMarketsListBatchFlowManager(
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
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
|
||||
|
|
@ -9,28 +9,36 @@ import com.tangem.core.ui.extensions.wrappedList
|
|||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import com.tangem.domain.core.utils.getOrElse
|
||||
import com.tangem.domain.core.utils.lceContent
|
||||
import com.tangem.domain.core.utils.lceError
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
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.tokens.GetTokenListUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
|
||||
import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapPairLeast
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
|
||||
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
|
||||
import com.tangem.features.swap.SwapFeatureToggles
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer
|
||||
|
|
@ -48,9 +56,17 @@ import com.tangem.features.onramp.tokenlist.entity.transformer.*
|
|||
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
|
||||
import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer
|
||||
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
|
@ -71,7 +87,10 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
|
||||
private val swapFeatureToggles: SwapFeatureToggles,
|
||||
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
swapFeatureToggles: SwapFeatureToggles,
|
||||
getWalletsUseCase: GetWalletsUseCase,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -79,6 +98,17 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
|
||||
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()
|
||||
var addToPortfolioManager: AddToPortfolioManager? = null
|
||||
val addToPortfolioCallback: AddToPortfolioComponent.Callback = object : AddToPortfolioComponent.Callback {
|
||||
override fun onDismiss() = bottomSheetNavigation.dismiss()
|
||||
override fun onSuccess(addedToken: CryptoCurrency) {
|
||||
onTokenAddedToPortfolio(addedToken)
|
||||
}
|
||||
}
|
||||
private val addToPortfolioJobHolder = JobHolder()
|
||||
|
||||
private val tokenListFlow = getTokenListUseCaseFlow()
|
||||
private val accountListFlow = getAccountListUseCaseFlow()
|
||||
|
|
@ -86,6 +116,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
|
||||
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())
|
||||
|
||||
|
|
@ -374,8 +405,12 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
|
||||
private fun subscribeOnAvailablePairsUpdates() {
|
||||
modelScope.launch {
|
||||
params.selectedStatus
|
||||
.filterNotNull()
|
||||
combine(
|
||||
params.selectedStatus.filterNotNull(),
|
||||
refreshPairsTrigger
|
||||
.onEach { availablePairsByNetworkFlow.value = emptyMap() }
|
||||
.onStart { emit(Unit) },
|
||||
) { status, _ -> status }
|
||||
.collectLatest { selectedStatus ->
|
||||
val networkInfo = selectedStatus.toLeastTokenInfo()
|
||||
|
||||
|
|
@ -542,7 +577,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
else -> SwapMarketState.Content(
|
||||
items = uiItems,
|
||||
loadMore = { searchMarketsListManager.loadMore() },
|
||||
onItemClick = {},
|
||||
onItemClick = { item -> addToPortfolioItem(item) },
|
||||
visibleIdsChanged = { visibleMarketItemIds.value = it },
|
||||
total = total ?: uiItems.size,
|
||||
)
|
||||
|
|
@ -556,6 +591,66 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onTokenAddedToPortfolio(addedToken: CryptoCurrency) {
|
||||
modelScope.launch {
|
||||
bottomSheetNavigation.dismiss()
|
||||
|
||||
// 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) {
|
||||
modelScope.launch {
|
||||
val tokenMarket = searchMarketsListManager.getTokenMarketById(item.id) ?: return@launch
|
||||
|
||||
val param = tokenMarket.toSerializableParam()
|
||||
val hasOnlyHotWallets = allUserWallets.all { it is UserWallet.Hot }
|
||||
|
||||
val networks = tokenMarket.networks?.filter { network ->
|
||||
BlockchainUtils.isSupportedNetworkId(
|
||||
blockchainId = network.networkId,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
hotExcludedBlockchains = hotWalletExcludedBlockchains,
|
||||
hasOnlyHotWallets = hasOnlyHotWallets,
|
||||
)
|
||||
}?.map { network ->
|
||||
TokenMarketInfo.Network(
|
||||
networkId = network.networkId,
|
||||
isExchangeable = false,
|
||||
contractAddress = network.contractAddress,
|
||||
decimalCount = network.decimalCount,
|
||||
)
|
||||
}.orEmpty()
|
||||
|
||||
addToPortfolioManager = addToPortfolioManagerFactory
|
||||
.create(
|
||||
scope = modelScope,
|
||||
token = param,
|
||||
analyticsParams = null,
|
||||
).apply {
|
||||
setTokenNetworks(networks)
|
||||
}
|
||||
|
||||
addToPortfolioManager?.state
|
||||
?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd }
|
||||
?.run { bottomSheetNavigation.activate(AddToPortfolioRoute) }
|
||||
}.saveIn(addToPortfolioJobHolder)
|
||||
}
|
||||
|
||||
private fun subscribeOnVisibleMarketItems() {
|
||||
modelScope.launch {
|
||||
visibleMarketItemIds.mapNotNull { rawIds ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue