Updated on 2026-08-14
This commit is contained in:
parent
f08687d985
commit
155b6516d9
6 changed files with 193 additions and 17 deletions
|
|
@ -2,7 +2,9 @@ package com.tangem.features.swap
|
|||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -15,6 +17,8 @@ interface SwapComponent : ComposableContentComponent {
|
|||
val isInitialReverseOrder: Boolean = false,
|
||||
val screenSource: String,
|
||||
val tangemPayInput: TangemPayInput? = null,
|
||||
val preselectedToToken: CryptoCurrencyStatus? = null,
|
||||
val preselectedAccount: Account.CryptoPortfolio? = null,
|
||||
) {
|
||||
data class TangemPayInput(
|
||||
val cryptoAmount: BigDecimal,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ dependencies {
|
|||
implementation(projects.common.uiMarkets)
|
||||
implementation(projects.common.uiCharts)
|
||||
|
||||
/** Data modules */
|
||||
implementation(projects.data.common)
|
||||
|
||||
/** Domain modules **/
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.appCurrency)
|
||||
|
|
@ -60,6 +63,7 @@ dependencies {
|
|||
implementation(projects.features.swap.api)
|
||||
implementation(projects.features.sendV2.api)
|
||||
implementation(projects.features.sendV2.impl)
|
||||
implementation(projects.features.feed.api)
|
||||
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||
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.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
|
|
@ -17,15 +18,18 @@ import com.tangem.common.ui.swapStoriesScreen.SwapStoriesScreen
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent
|
||||
import com.tangem.feature.swap.model.SwapModel
|
||||
import com.tangem.feature.swap.models.AddToPortfolioRoute
|
||||
import com.tangem.feature.swap.models.SwapCardState.SwapCardData
|
||||
import com.tangem.feature.swap.router.SwapNavScreen
|
||||
import com.tangem.feature.swap.ui.SwapScreen
|
||||
import com.tangem.feature.swap.ui.SwapSelectTokenScreen
|
||||
import com.tangem.feature.swap.ui.SwapSuccessScreen
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
|
||||
import com.tangem.features.send.v2.api.SendFeatureToggles
|
||||
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.swap.SwapComponent
|
||||
|
|
@ -39,10 +43,19 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
@Assisted private val params: SwapComponent.Params,
|
||||
private val swapFeeSelectorBlockComponentFactory: SwapFeeSelectorBlockComponent.Factory,
|
||||
private val sendFeatureToggles: SendFeatureToggles,
|
||||
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
|
||||
) : SwapComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SwapModel = getOrCreateModel(params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = AddToPortfolioRoute.serializer(),
|
||||
key = BOTTOM_SHEET_SLOT_KEY,
|
||||
handleBackButton = false,
|
||||
childFactory = { configuration, context -> bottomSheetChild(context) },
|
||||
)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onStart = model::onStart,
|
||||
|
|
@ -54,6 +67,7 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
val childSlot = childSlot(
|
||||
source = slotNavigation,
|
||||
serializer = null,
|
||||
key = FEE_SELECTOR_SLOT_KEY,
|
||||
childFactory = { config, context ->
|
||||
createSwapFeeSelectorBlockComponent(
|
||||
context = childByContext(context),
|
||||
|
|
@ -114,6 +128,7 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
|
||||
val feeSelectorChildStackState by childSlot.subscribeAsState()
|
||||
val feeSelectorBlockComponent = feeSelectorChildStackState.child?.instance
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
Crossfade(
|
||||
modifier = Modifier.background(TangemTheme.colors.background.secondary),
|
||||
|
|
@ -165,10 +180,29 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@Suppress("UnsafeCallOnNullableType")
|
||||
private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent {
|
||||
return addToPortfolioComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AddToPortfolioComponent.Params(
|
||||
addToPortfolioManager = model.addToPortfolioManager!!,
|
||||
callback = model.addToPortfolioCallback,
|
||||
shouldSkipTokenActionsScreen = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SwapComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: SwapComponent.Params): DefaultSwapComponent
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val BOTTOM_SHEET_SLOT_KEY = "bottomSheetSlot"
|
||||
const val FEE_SELECTOR_SLOT_KEY = "feeSelectorSlot"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class TokenMarketInfoToParamsConverter : Converter<TokenMarketInfo, TokenMarketParams> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo): TokenMarketParams {
|
||||
val tokenId = CryptoCurrency.RawID(value.id)
|
||||
return TokenMarketParams(
|
||||
id = tokenId,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
tokenQuotes = TokenMarketParams.Quotes(
|
||||
currentPrice = value.quotes.currentPrice,
|
||||
h24Percent = value.quotes.h24ChangePercent,
|
||||
weekPercent = value.quotes.weekChangePercent,
|
||||
monthPercent = value.quotes.monthChangePercent,
|
||||
),
|
||||
imageUrl = getTokenIconUrlFromDefaultHost(tokenId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,10 +6,15 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.setValue
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState.InProgress.getApproveTypeOrNull
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
|
@ -28,6 +33,7 @@ import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
|||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
|
||||
import com.tangem.domain.express.models.ExpressOperationType
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||
|
|
@ -35,10 +41,12 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
|||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.WithdrawalResult
|
||||
import com.tangem.domain.promo.GetStoryContentUseCase
|
||||
|
|
@ -58,9 +66,11 @@ import com.tangem.domain.transaction.models.TransactionFeeExtended
|
|||
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.feature.swap.analytics.StoriesEvents
|
||||
import com.tangem.feature.swap.analytics.SwapEvents
|
||||
import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent
|
||||
import com.tangem.feature.swap.converters.TokenMarketInfoToParamsConverter
|
||||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
import com.tangem.feature.swap.domain.TransactionFeeResult
|
||||
import com.tangem.feature.swap.domain.TxFeeSealedState
|
||||
|
|
@ -69,6 +79,7 @@ import com.tangem.feature.swap.domain.models.ExpressException
|
|||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import com.tangem.feature.swap.models.AddToPortfolioRoute
|
||||
import com.tangem.feature.swap.models.SwapCardState
|
||||
import com.tangem.feature.swap.models.SwapStateHolder
|
||||
import com.tangem.feature.swap.models.UiActions
|
||||
|
|
@ -80,11 +91,14 @@ import com.tangem.feature.swap.router.SwapNavScreen
|
|||
import com.tangem.feature.swap.router.SwapRouter
|
||||
import com.tangem.feature.swap.ui.StateBuilder
|
||||
import com.tangem.feature.swap.utils.formatToUIRepresentation
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager
|
||||
import com.tangem.features.send.v2.api.SendFeatureToggles
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.swap.SwapComponent
|
||||
import com.tangem.features.swap.SwapFeatureToggles
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
|
||||
import com.tangem.utils.coroutines.*
|
||||
|
|
@ -100,6 +114,7 @@ import java.text.DecimalFormat
|
|||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.filter
|
||||
|
||||
typealias SuccessLoadedSwapData = Map<SwapProvider, SwapState.QuotesLoadedState>
|
||||
|
||||
|
|
@ -137,7 +152,11 @@ internal class SwapModel @Inject constructor(
|
|||
private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger,
|
||||
private val sendFeatureToggles: SendFeatureToggles,
|
||||
private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
|
||||
private val swapFeatureToggles: SwapFeatureToggles,
|
||||
swapFeatureToggles: SwapFeatureToggles,
|
||||
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
|
||||
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val getUserWalletsUseCase: GetWalletsUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<SwapComponent.Params>()
|
||||
|
|
@ -214,6 +233,7 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
private val fromTokenBalanceJobHolder = JobHolder()
|
||||
private val toTokenBalanceJobHolder = JobHolder()
|
||||
private val addToPortfolioJobHolder = JobHolder()
|
||||
|
||||
private var isAmountChangedByUser: Boolean = false
|
||||
private var lastPermissionNotificationTokens: Pair<String, String>? = null
|
||||
|
|
@ -234,6 +254,24 @@ internal class SwapModel @Inject constructor(
|
|||
val currentScreen: SwapNavScreen
|
||||
get() = swapRouter.currentScreen
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute> = SlotNavigation()
|
||||
val addToPortfolioCallback = object : AddToPortfolioComponent.Callback {
|
||||
override fun onDismiss() = bottomSheetNavigation.dismiss()
|
||||
|
||||
override fun onSuccess(addedToken: CryptoCurrency) {
|
||||
modelScope.launch {
|
||||
bottomSheetNavigation.dismiss()
|
||||
getAccountCurrencyStatusUseCase.invoke(userWalletId, addedToken)
|
||||
.firstOrNull {
|
||||
it.status.value is CryptoCurrencyStatus.Loaded
|
||||
}?.let { (account, status) ->
|
||||
applyAddedToken(status, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var addToPortfolioManager: AddToPortfolioManager? = null
|
||||
|
||||
init {
|
||||
userCountry = getUserCountryUseCase.invokeSync().getOrNull()
|
||||
?: UserCountry.Other(Locale.getDefault().country)
|
||||
|
|
@ -318,7 +356,7 @@ internal class SwapModel @Inject constructor(
|
|||
items = uiItems,
|
||||
loadMore = { searchMarketsListManager.loadMore() },
|
||||
onItemClick = { item ->
|
||||
// TODO [REDACTED_TASK_KEY] Add currency to swap form market list
|
||||
addToPortfolioItem(item)
|
||||
},
|
||||
visibleIdsChanged = { visibleMarketItemIds.value = it },
|
||||
)
|
||||
|
|
@ -424,21 +462,7 @@ internal class SwapModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
(dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { coin ->
|
||||
subscribeToCoinBalanceUpdates(
|
||||
userWalletId = userWalletId,
|
||||
coin = coin,
|
||||
isFromCurrency = true,
|
||||
)
|
||||
}
|
||||
|
||||
(dataState.toCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { coin ->
|
||||
subscribeToCoinBalanceUpdates(
|
||||
userWalletId = userWalletId,
|
||||
coin = coin,
|
||||
isFromCurrency = false,
|
||||
)
|
||||
}
|
||||
subscribeToCoinBalanceUpdatesIfNeeded()
|
||||
}.onFailure { error ->
|
||||
Timber.e(error)
|
||||
|
||||
|
|
@ -464,6 +488,47 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun applyAddedToken(addedToken: CryptoCurrencyStatus, addedAccount: Account.CryptoPortfolio?) {
|
||||
modelScope.launch {
|
||||
runCatching(dispatchers.io) {
|
||||
swapInteractor.getTokensDataState(initialCurrencyFrom)
|
||||
}.onSuccess { state ->
|
||||
updateTokensState(state)
|
||||
|
||||
applyInitialTokenChoice(
|
||||
state = state,
|
||||
selectedCurrency = addedToken,
|
||||
selectedAccount = addedAccount,
|
||||
isReverseFromTo = isOrderReversed,
|
||||
)
|
||||
|
||||
subscribeToCoinBalanceUpdatesIfNeeded()
|
||||
|
||||
swapRouter.back()
|
||||
}.onFailure { error ->
|
||||
Timber.e(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToCoinBalanceUpdatesIfNeeded() {
|
||||
(dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { coin ->
|
||||
subscribeToCoinBalanceUpdates(
|
||||
userWalletId = userWalletId,
|
||||
coin = coin,
|
||||
isFromCurrency = true,
|
||||
)
|
||||
}
|
||||
|
||||
(dataState.toCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { coin ->
|
||||
subscribeToCoinBalanceUpdates(
|
||||
userWalletId = userWalletId,
|
||||
coin = coin,
|
||||
isFromCurrency = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initStories() {
|
||||
modelScope.launch {
|
||||
getStoryContentUseCase.invokeSync(StoryContentIds.STORY_FIRST_TIME_SWAP.id).fold(
|
||||
|
|
@ -1933,6 +1998,42 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun addToPortfolioItem(item: MarketsListItemUM) {
|
||||
modelScope.launch {
|
||||
val tokenInfo = getTokenMarketInfoUseCase(
|
||||
selectedAppCurrencyFlow.value,
|
||||
item.id,
|
||||
item.currencySymbol,
|
||||
).getOrNull() ?: return@launch
|
||||
|
||||
val converter = TokenMarketInfoToParamsConverter()
|
||||
val param = converter.convert(tokenInfo)
|
||||
val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot }
|
||||
|
||||
val networks = tokenInfo.networks?.filter { network ->
|
||||
BlockchainUtils.isSupportedNetworkId(
|
||||
blockchainId = network.networkId,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
hotExcludedBlockchains = hotWalletExcludedBlockchains,
|
||||
hasOnlyHotWallets = hasOnlyHotWallets,
|
||||
)
|
||||
}.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 CryptoCurrency.getNetworkInfo(): NetworkInfo {
|
||||
return NetworkInfo(
|
||||
name = this.network.name,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.feature.swap.models
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data object AddToPortfolioRoute : Route
|
||||
Loading…
Add table
Add a link
Reference in a new issue