Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-14 13:57:57 +03:00
commit f3dfb262c2
527 changed files with 12898 additions and 3301 deletions

View file

@ -4,6 +4,7 @@ import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -23,14 +24,14 @@ internal interface ChooseTokenBridge {
val onClose: Channel<Unit>
// todo swap legacy api, remove
val onTokenSelected: Channel<Pair<String, ChooseTokenAnalyticsPayload.IsSearched>>
val onTokenSelected: Channel<ChooseTokenResultOld>
val onNewTokenAdded: Channel<Pair<CryptoCurrency, ChooseTokenAnalyticsPayload.IsSearched>>
val searchQueryState: StateFlow<String>
val currenciesGroup: Flow<CurrenciesGroup>
fun onTokenSelected(tokenId: Pair<String, ChooseTokenAnalyticsPayload.IsSearched>) {
onTokenSelected.trySend(tokenId)
fun onTokenSelected(result: ChooseTokenResultOld) {
onTokenSelected.trySend(result)
onSearchQuery("")
}
@ -57,6 +58,12 @@ internal interface ChooseTokenBridge {
}
}
data class ChooseTokenResultOld(
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val account: Account,
val isSearched: Boolean,
)
data class ChooseTokenResult(
val currency: CryptoCurrencyStatus,
val account: AccountStatus,

View file

@ -4,6 +4,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge
import com.tangem.feature.swap.choosetoken.api.ChooseTokenResult
import com.tangem.feature.swap.choosetoken.api.ChooseTokenResultOld
import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel.Companion.DEBOUNCE_SEARCH_DELAY
import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup
import dagger.assisted.Assisted
@ -19,7 +20,7 @@ internal class DefaultChooseTokenBridge @AssistedInject constructor(
override val onCurrencyChosen: Channel<ChooseTokenResult> = Channel()
override val onTokenSelected: Channel<Pair<String, ChooseTokenAnalyticsPayload.IsSearched>> = Channel()
override val onTokenSelected: Channel<ChooseTokenResultOld> = Channel()
override val onNewTokenAdded: Channel<Pair<CryptoCurrency, ChooseTokenAnalyticsPayload.IsSearched>> = Channel()
override val onClose: Channel<Unit> = Channel()

View file

@ -15,15 +15,15 @@ import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent
import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel
import com.tangem.feature.swap.models.AddToPortfolioRoute
import com.tangem.feature.swap.ui.SwapSelectTokenScreen
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultChooseTokenComponent @AssistedInject constructor(
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: ChooseTokenComponent.Params,
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
) : AppComponentContext by appComponentContext, ChooseTokenComponent {
private val model: ChooseTokenModel = getOrCreateModel(params)

View file

@ -6,9 +6,12 @@ import com.tangem.common.ui.tokens.TokenConverterParams
import com.tangem.common.ui.tokens.TokenItemGrouping.toGroupedItems
import com.tangem.common.ui.tokens.TokenItemGrouping.toUngroupedItems
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.core.ui.components.icons.IconTint
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
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.account.AccountStatus
import com.tangem.domain.models.account.filterCryptoPortfolio
@ -17,6 +20,7 @@ import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.feature.swap.choosetoken.impl.model.ClickIntents
import com.tangem.feature.swap.choosetoken.impl.model.isSearchingState
import com.tangem.feature.swap.models.TokenListUMData
import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.toPersistentList
internal class ChooseTokenListItemConverter(
@ -57,10 +61,9 @@ internal class ChooseTokenListItemConverter(
if (accountItems.isEmpty()) {
return TokenListUMData.EmptyList
}
val accountsList = accountItems.toPersistentList()
return TokenListUMData.AccountList(
tokensList = accountsList,
totalTokensCount = accountsList.size,
tokensList = accountItems.toPersistentList(),
totalTokensCount = accountItems.sumOf { portfolio -> portfolio.tokensItemsList.size },
)
}
@ -77,11 +80,19 @@ internal class ChooseTokenListItemConverter(
clickIntents.onAccountExpandClick(clickedAccount)
}
}
val fiatAmountStateProvider: ((TotalFiatBalance) -> FiatAmountState?) = if (isSearchingState) {
{ _ -> FiatAmountState.Empty }
} else {
{ _ -> FiatAmountState.Icon(R.drawable.ic_chewron_down_20, IconTint.Informative) }
}
val converter = AccountCryptoPortfolioItemStateConverter(
appCurrency = appCurrency,
account = account,
onItemClick = onItemClick.takeIf { !isSearchingState },
priceChangeLce = this.priceChangeLce,
fiatAmountStateProvider = fiatAmountStateProvider,
subtitle2StateProvider = { _ -> null },
)
val accountItem = converter.convert(tokenList.totalFiatBalance)
val tokenConverter = tokenStatusConverter(this)
@ -100,18 +111,14 @@ internal class ChooseTokenListItemConverter(
return when (tokenList) {
is TokenList.Empty -> TokenListUMData.EmptyList
is TokenList.GroupedByNetwork -> tokenList.toGroupedItems(tokenConverter).let { grouped ->
TokenListUMData.TokenList(
tokensList = grouped.toPersistentList(),
totalTokensCount = grouped.size,
)
}
is TokenList.Ungrouped -> tokenList.toUngroupedItems(tokenConverter).let { ungrouped ->
TokenListUMData.TokenList(
tokensList = ungrouped.toPersistentList(),
totalTokensCount = ungrouped.size,
)
}
is TokenList.GroupedByNetwork -> TokenListUMData.TokenList(
tokensList = tokenList.toGroupedItems(tokenConverter).toPersistentList(),
totalTokensCount = tokenList.flattenCurrencies().size,
)
is TokenList.Ungrouped -> TokenListUMData.TokenList(
tokensList = tokenList.toUngroupedItems(tokenConverter).toPersistentList(),
totalTokensCount = tokenList.flattenCurrencies().size,
)
}
}

View file

@ -5,8 +5,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.AccountId
@ -19,16 +18,12 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.swap.choosetoken.api.*
import com.tangem.feature.swap.choosetoken.impl.converter.SearchBarToggleTransformer
import com.tangem.feature.swap.choosetoken.impl.converter.SearchBarUpdateQueryTransformer
import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenFullUM
import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenInitialUM
import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenUM
import com.tangem.feature.swap.choosetoken.impl.ui.WalletListUM
import com.tangem.feature.swap.choosetoken.impl.ui.*
import com.tangem.feature.swap.converters.TokensDataConverter
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
import com.tangem.feature.swap.models.TokenListUMData
import com.tangem.feature.swap.models.market.state.SwapMarketState
import com.tangem.feature.swap.presentation.R
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@ -121,10 +116,13 @@ internal class ChooseTokenModel @Inject constructor(
TokensDataConverter(
onSearchEntered = { query -> bridge.onSearchQuery(query) },
onTokenClick = { tokenId ->
val selected = tokenId to ChooseTokenAnalyticsPayload
.IsSearched(isSearchingState)
bridge.onTokenSelected(selected)
onTokenClick = { account, cryptoCurrencyStatus ->
val result = ChooseTokenResultOld(
account = account,
cryptoCurrencyStatus = cryptoCurrencyStatus,
isSearched = searchQueryState.value.isNotEmpty(),
)
bridge.onTokenSelected(result)
},
onAccountClick = { account ->
expandedAccountsFlow.update { expandedList ->
@ -147,41 +145,39 @@ internal class ChooseTokenModel @Inject constructor(
@Suppress("LongMethod")
private fun combineUI(): StateFlow<ChooseTokenUM?> = channelFlow {
val allWalletsFlow: StateFlow<LinkedHashMap<UserWalletId, UserWallet>> =
val allWalletsFlow: StateFlow<Map<UserWalletId, UserWallet>> =
getWalletsUseCase.invokeAsMap().stateIn(this)
// todo swap add optional param, store, and GetSelectedWalletUseCase
val firstSelectedWallet = allWalletsFlow.value.values.first()
val selectedWalletFlow: StateFlow<UserWallet> =
onWalletSelected.receiveAsFlow()
.mapNotNull { walletId -> allWalletsFlow.value[walletId] }
.stateIn(this, SharingStarted.Eagerly, allWalletsFlow.value.values.first())
.stateIn(this, SharingStarted.Eagerly, firstSelectedWallet)
val selectedWalletTokensData: Flow<TokenListUMData> = combine(
flow = selectedWalletFlow.map { wallet -> wallet.walletId }.distinctUntilChanged(),
val fullPortfolioBlockFlow = combine(
flow = allWalletsFlow,
flow2 = portfolioListBlockDelegate.portfolioList,
transform = { selectedWalletId, allPortfoliosData -> allPortfoliosData[selectedWalletId] },
)
.filterNotNull()
.distinctUntilChanged()
val walletListUmFlow = combine(
flow = selectedWalletFlow,
flow2 = allWalletsFlow,
transform = { selectedWallet, allWallets ->
allWallets.entries
flow3 = selectedWalletFlow.map { wallet -> wallet.walletId }.distinctUntilChanged(),
transform = { allWallets, portfolioList, selectedWalletId ->
val tokensListData = portfolioList[selectedWalletId] ?: return@combine null
val walletsUM = allWallets.entries
.map { (walletId, wallet) ->
val type = if (selectedWallet.walletId == walletId) {
TangemButtonType.Primary
} else {
TangemButtonType.Secondary
}
TangemButtonUM(
val searchResultCount: TextReference? = portfolioList[walletId]?.totalTokensCount
?.toString()
?.let(::stringReference)
?.takeIf { isSearchingState }
WalletTabUM(
text = stringReference(wallet.name),
onClick = { onWalletSelected.trySend(walletId) },
type = type,
isSelected = selectedWalletId == walletId,
count = searchResultCount,
)
}
walletsUM to tokensListData
},
)
.filterNotNull()
.distinctUntilChanged()
portfolioListBlockDelegate.onTokenItemClick.receiveAsFlow()
@ -195,11 +191,10 @@ internal class ChooseTokenModel @Inject constructor(
.launchIn(this)
combine(
flow = selectedWalletTokensData,
flow = fullPortfolioBlockFlow,
flow2 = settingContextUseCase.invoke(),
flow3 = marketsStateFlow,
flow4 = walletListUmFlow,
transform = { tokensData, settings, marketsData, walletList ->
transform = { (walletList, tokensData), settings, marketsData ->
val walletsUM = if (walletList.size != 1) {
WalletListUM(walletList.toPersistentList())
} else {

View file

@ -17,7 +17,7 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.swap.models.AddToPortfolioRoute
import com.tangem.feature.swap.models.market.MarketsListBatchFlowManager
import com.tangem.feature.swap.models.market.state.SwapMarketState
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.JobHolder
@ -32,9 +32,9 @@ import kotlinx.coroutines.launch
@Suppress("LongParameterList")
internal class MarketBlockDelegate @AssistedInject constructor(
private val marketsListBatchFlowManagerFactory: MarketsListBatchFlowManager.Factory,
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
private val excludedBlockchains: ExcludedBlockchains,
private val getUserWalletsUseCase: GetWalletsUseCase,
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
@Assisted private val modelScope: CoroutineScope,
@Assisted private val searchQueryState: StateFlow<String>,
@Assisted private val screensSourcesName: String,
@ -187,7 +187,7 @@ internal class MarketBlockDelegate @AssistedInject constructor(
val networks = tokenMarket.networks?.filter { network ->
BlockchainUtils.isSupportedNetworkId(
blockchainId = network.networkId,
networkId = network.networkId,
coinId = tokenMarket.id.value,
contractAddress = network.contractAddress,
excludedBlockchains = excludedBlockchains,

View file

@ -1,15 +1,23 @@
package com.tangem.feature.swap.choosetoken.impl.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@ -17,10 +25,6 @@ import androidx.compose.ui.unit.dp
import com.tangem.common.ui.tokens.portfolioTokensList
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
@ -34,8 +38,6 @@ import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -54,6 +56,18 @@ import kotlin.random.Random
private const val LOAD_MORE_BUFFER = 25
private val ChooseTokenUM.isNotFoundState: Boolean
get() = tokensListData.tokensList.isEmpty() &&
isSearching &&
marketsState !is SwapMarketState.Content &&
marketsState !is SwapMarketState.Loading
private val ChooseTokenUM.isEmptyState: Boolean
get() = tokensListData.tokensList.isEmpty() &&
!isSearching &&
marketsState !is SwapMarketState.Content &&
marketsState !is SwapMarketState.Loading
@Composable
internal fun ChooseTokenScreen(state: ChooseTokenFullUM, modifier: Modifier = Modifier) {
Column(
@ -93,7 +107,6 @@ private fun Content(state: ChooseTokenFullUM, modifier: Modifier = Modifier) {
modifier = modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection),
horizontalAlignment = Alignment.CenterHorizontally,
state = lazyListState,
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
) {
@ -108,20 +121,26 @@ private fun Content(state: ChooseTokenFullUM, modifier: Modifier = Modifier) {
assetsTitle()
if (state.contentUM != null) {
walletListItem(state.contentUM.walletList)
when {
state.contentUM.isNotFoundState -> tokensNotFound()
state.contentUM.isEmptyState -> emptyTokensList()
else -> {
walletListItem(state.contentUM.walletList)
tokensListItems(
tokensListData = state.contentUM.tokensListData,
isBalanceHidden = state.contentUM.isBalanceHidden,
)
tokensListItems(
tokensListData = state.contentUM.tokensListData,
isBalanceHidden = state.contentUM.isBalanceHidden,
)
if (state.contentUM.marketsState != null) {
item("markets_title_spacer") { SpacerH(height = 20.dp) }
swapMarketsListItems(state.contentUM.marketsState)
if (state.contentUM.marketsState != null) {
item("markets_title_spacer") { SpacerH(height = 20.dp) }
swapMarketsListItems(state.contentUM.marketsState)
}
}
}
}
}
if (state.contentUM?.marketsState != null) {
if (state.contentUM?.marketsState != null && !state.contentUM.isNotFoundState && !state.contentUM.isEmptyState) {
SetupMarketScrollTracker(state.contentUM.marketsState, lazyListState)
}
}
@ -183,26 +202,59 @@ private fun LazyListScope.assetsTitle() {
private fun LazyListScope.walletListItem(walletList: WalletListUM) {
if (walletList.items.isEmpty()) return
item("wallet_list") {
Row(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
LazyRow(
modifier = Modifier.padding(top = 12.dp, bottom = 4.dp),
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(horizontal = 16.dp),
) {
walletList.items.forEach { um ->
val colors = when (um.type) {
TangemButtonType.Primary -> TangemButtonsDefaults.primaryButtonColors
else -> TangemButtonsDefaults.secondaryButtonColors
}
TangemButton(
text = um.text?.resolveReference().orEmpty(),
icon = TangemButtonIconPosition.None,
size = TangemButtonSize.Action,
colors = colors,
showProgress = false,
onClick = um.onClick,
enabled = true,
items(walletList.items) { um ->
WalletTabItem(um)
}
}
}
}
@Composable
private fun WalletTabItem(state: WalletTabUM, modifier: Modifier = Modifier) {
val isSelected = state.isSelected
val backgroundColor = if (isSelected) TangemTheme.colors.button.primary else TangemTheme.colors.button.secondary
val buttonTextColor = if (isSelected) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1
val countTextColor = if (isSelected) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.secondary
val countBackground = if (isSelected) {
TangemTheme.colors.button.secondary.copy(alpha = 0.2f)
} else {
TangemTheme.colors.button.primary.copy(alpha = 0.1f)
}
Row(
modifier = modifier
.clip(RoundedCornerShape(12.dp))
.background(backgroundColor)
.clickable(onClick = state.onClick)
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = state.text.resolveReference(),
color = buttonTextColor,
style = TangemTheme.typography.button,
)
if (state.count != null) {
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier
.background(countBackground, shape = CircleShape)
.defaultMinSize(minWidth = 20.dp)
.padding(horizontal = 4.dp, vertical = 2.dp),
contentAlignment = Alignment.Center,
) {
Text(
text = state.count.resolveReference(),
color = countTextColor,
style = TangemTheme.typography.caption1,
)
}
}
@ -253,6 +305,58 @@ private fun LazyListScope.tokensList(items: ImmutableList<TokensListItemUM>, isB
)
}
private fun LazyListScope.emptyTokensList(modifier: Modifier = Modifier) {
item("EmptyTokensList") {
Box(
modifier = modifier
.background(TangemTheme.colors.background.secondary)
.fillParentMaxSize(),
) {
Column(modifier = Modifier.align(Alignment.Center)) {
Image(
modifier = Modifier
.size(TangemTheme.dimens.size64)
.align(Alignment.CenterHorizontally),
painter = painterResource(id = R.drawable.ic_no_token_44),
colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive),
contentDescription = null,
)
Text(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing16)
.padding(horizontal = TangemTheme.dimens.spacing30)
.align(Alignment.CenterHorizontally),
text = stringResourceSafe(id = R.string.exchange_tokens_empty_tokens),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
}
}
}
}
private fun LazyListScope.tokensNotFound(modifier: Modifier = Modifier) {
item("TokensNotFound") {
Box(
modifier = modifier
.background(TangemTheme.colors.background.secondary)
.fillParentMaxSize(),
) {
Text(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing32)
.padding(horizontal = TangemTheme.dimens.spacing30)
.align(Alignment.TopCenter),
text = stringResourceSafe(id = R.string.express_token_list_empty_search),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
}
}
}
@Preview
@Composable
private fun TokenScreenPreview(@PreviewParameter(ChooseTokenScreenPreviewProvider::class) state: ChooseTokenFullUM) {
@ -321,31 +425,42 @@ private val accounts
private val wallets
get() = persistentListOf(
TangemButtonUM(
WalletTabUM(
text = TextReference.Str(value = "Wallet 1"),
type = TangemButtonType.Primary,
isSelected = true,
onClick = {},
count = null,
),
TangemButtonUM(
WalletTabUM(
text = TextReference.Str(value = "Wallet 1"),
isSelected = true,
onClick = {},
count = stringReference("3"),
),
WalletTabUM(
text = TextReference.Str(value = "Wallet 2"),
type = TangemButtonType.Secondary,
isSelected = false,
onClick = {},
count = stringReference("333"),
),
TangemButtonUM(
WalletTabUM(
text = TextReference.Str(value = "Wallet 3"),
type = TangemButtonType.Secondary,
isSelected = false,
onClick = {},
count = null,
),
)
private val initialUM = ChooseTokenInitialUM(
screenTitle = stringReference("Choose token"),
onCloseClick = {},
searchBar = searchBar,
)
private class ChooseTokenScreenPreviewProvider : PreviewParameterProvider<ChooseTokenFullUM> {
override val values: Sequence<ChooseTokenFullUM> = sequenceOf(
ChooseTokenFullUM(
initialUM = ChooseTokenInitialUM(
screenTitle = stringReference("Choose token"),
onCloseClick = {},
searchBar = searchBar,
),
initialUM = initialUM,
contentUM = ChooseTokenUM(
walletList = WalletListUM(wallets),
isBalanceHidden = false,
@ -357,5 +472,15 @@ private class ChooseTokenScreenPreviewProvider : PreviewParameterProvider<Choose
marketsState = SwapSelectTokenPreviewProvider.defaultState.marketsState,
),
),
ChooseTokenFullUM(
initialUM = initialUM,
contentUM = ChooseTokenUM(
walletList = WalletListUM(wallets),
isBalanceHidden = false,
isSearching = false,
tokensListData = TokenListUMData.EmptyList,
marketsState = null,
),
),
)
}

View file

@ -1,7 +1,6 @@
package com.tangem.feature.swap.choosetoken.impl.ui
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.swap.models.TokenListUMData
import com.tangem.feature.swap.models.market.state.SwapMarketState
@ -27,5 +26,12 @@ internal data class ChooseTokenInitialUM(
)
internal data class WalletListUM(
val items: ImmutableList<TangemButtonUM>,
val items: ImmutableList<WalletTabUM>,
)
internal data class WalletTabUM(
val text: TextReference,
val count: TextReference?,
val isSelected: Boolean,
val onClick: () -> Unit,
)

View file

@ -2,15 +2,21 @@ package com.tangem.feature.swap.converters
import com.tangem.common.getTotalCryptoAmount
import com.tangem.common.getTotalFiatAmount
import com.tangem.common.ui.R
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
import com.tangem.common.ui.account.TokensListPortfolioItemConverter
import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
@ -29,34 +35,68 @@ internal class AccountTokenItemConverter(
private val appCurrency: AppCurrency,
private val unavailableErrorText: TextReference,
private val expandedAccounts: Map<AccountId, Boolean>,
private val onTokenItemClick: (String) -> Unit,
private val onAccountItemClick: (Account.CryptoPortfolio) -> Unit,
private val onTokenItemClick: (Account, CryptoCurrencyStatus) -> Unit,
private val onAccountItemClick: (Account) -> Unit,
) : Converter<AccountSwapAvailability, TokensListItemUM.Portfolio> {
override fun convert(value: AccountSwapAvailability): TokensListItemUM.Portfolio {
return TokensListPortfolioItemConverter(
tokenItemUM = AccountCryptoPortfolioItemStateConverter(
val headerTokenItemState = when (val account = value.account) {
is Account.CryptoPortfolio -> AccountCryptoPortfolioItemStateConverter(
appCurrency = appCurrency,
account = value.account.copy(
cryptoCurrencies = value.currencyList.map { it.cryptoCurrencyStatus.currency },
),
account = account.copy(cryptoCurrencies = value.currencyList.map { it.cryptoCurrencyStatus.currency }),
onItemClick = onAccountItemClick,
).convert(
TotalFiatBalance.Loaded(
amount = value.currencyList.sumOf { it.cryptoCurrencyStatus.value.fiatAmount.orZero() },
source = StatusSource.ONLY_CACHE,
),
),
)
is Account.Payment -> createPaymentAccountHeaderState(value)
}
return TokensListPortfolioItemConverter(
tokenItemUM = headerTokenItemState,
isExpanded = expandedAccounts[value.account.accountId] != false,
isCollapsable = true,
tokens = value.currencyList.map { accountSwapCurrency ->
createAvailableItemConverter()
createAvailableItemConverter(value.account)
.convert(accountSwapCurrency.cryptoCurrencyStatus)
}.map(TokensListItemUM::Token).toPersistentList(),
).convert(Unit)
}
fun createAvailableItemConverter(): TokenItemStateConverter {
private fun createPaymentAccountHeaderState(accountSwapAvailability: AccountSwapAvailability): TokenItemState {
val account = accountSwapAvailability.account
val tokensCount = accountSwapAvailability.currencyList.size
val fiatBalance =
accountSwapAvailability.currencyList.sumOf { it.cryptoCurrencyStatus.value.fiatAmount.orZero() }
return TokenItemState.Content(
id = account.accountId.value,
iconState = CurrencyIconState.PaymentAccount(),
titleState = TokenItemState.TitleState.Content(text = account.accountName.toUM().value),
subtitleState = TokenItemState.SubtitleState.TextContent(
value = pluralReference(
R.plurals.common_tokens_count,
count = tokensCount,
formatArgs = wrappedList(tokensCount),
),
isAvailable = false,
),
onItemClick = { onAccountItemClick(account) },
fiatAmountState = FiatAmountState.Content(
text = fiatBalance.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
isFlickering = false,
),
subtitle2State = null,
onItemLongClick = null,
)
}
fun createAvailableItemConverter(account: Account): TokenItemStateConverter {
return TokenItemStateConverter(
appCurrency = appCurrency,
subtitleStateProvider = { status ->
@ -70,7 +110,7 @@ internal class AccountTokenItemConverter(
fiatAmountStateProvider = {
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = true)
},
onItemClick = { account, currencyStatus -> onTokenItemClick(currencyStatus.currency.id.value) },
onItemClick = { _, currencyStatus -> onTokenItemClick(account, currencyStatus) },
)
}

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
import com.tangem.feature.swap.models.TokenListUMData
@ -16,10 +17,10 @@ import kotlinx.collections.immutable.toPersistentList
@Suppress("LongParameterList")
internal class TokensDataConverter(
private val onSearchEntered: (String) -> Unit,
onTokenClick: (String) -> Unit,
onAccountClick: (Account.CryptoPortfolio) -> Unit,
onTokenClick: (Account, CryptoCurrencyStatus) -> Unit,
onAccountClick: (Account) -> Unit,
private val expandedAccounts: Map<AccountId, Boolean>,
private val onSearchEntered: (String) -> Unit,
private val tokensDataState: CurrenciesGroup,
private val isBalanceHidden: Boolean,
private val isAccountsMode: Boolean,
@ -52,7 +53,7 @@ internal class TokensDataConverter(
} else {
val tokensList = accountList.flatMap { (_, currencyList) ->
currencyList.asSequence().map { accountSwapCurrency ->
accountListItemConverter.createAvailableItemConverter()
accountListItemConverter.createAvailableItemConverter(accountSwapCurrency.account)
.convert(accountSwapCurrency.cryptoCurrencyStatus)
}.map(TokensListItemUM::Token).toPersistentList()
}.toPersistentList()

View file

@ -33,7 +33,6 @@ import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.utils.InputNumberFormatter
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
@ -49,12 +48,14 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.derivationIndex
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.UserWalletId
import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.domain.pay.WithdrawalResult
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
import com.tangem.domain.promo.ShouldShowStoriesUseCase
import com.tangem.domain.promo.models.StoryContentIds
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
@ -146,6 +147,7 @@ internal class SwapModel @Inject constructor(
private val appsFlyerStore: AppsFlyerStore,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
private val messageSender: UiMessageSender,
private val paymentAccountCryptoCurrencyStatusUseCase: GetPaymentAccountCryptoCurrencyStatusUseCase,
private val allowPermissionsHandler: AllowPermissionsHandler,
chooseTokenBridgeFactory: ChooseTokenBridge.Factory,
giveApprovalFeatureToggles: GiveApprovalFeatureToggles,
@ -222,8 +224,10 @@ internal class SwapModel @Inject constructor(
private val swapRouter: SwapRouter = SwapRouter(router = router)
private var userCountry: UserCountry? = null
private var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null
private var toAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null
private var fromAccount: Account? = null
private var toAccount: Account? = null
private var fromAccountStatus: CryptoCurrencyStatus? = null
private var toAccountStatus: CryptoCurrencyStatus? = null
/**
* If user came from Tangem Pay -> fromAccountCurrencyStatus == null
@ -293,8 +297,12 @@ internal class SwapModel @Inject constructor(
.launchIn(modelScope)
chooseTokenBridge.onTokenSelected.receiveAsFlow()
.onEach { (addedToken, isSearched) ->
onTokenSelect(addedToken, isSearched.value)
.onEach { result ->
onTokenSelect(
account = result.account,
cryptoCurrencyStatus = result.cryptoCurrencyStatus,
isSearched = result.isSearched,
)
}
.launchIn(modelScope)
@ -329,22 +337,31 @@ internal class SwapModel @Inject constructor(
userWalletId = userWalletId,
currency = initialCurrencyFrom,
).getOrNull()
val fromPaymentAccountStatus =
paymentAccountCryptoCurrencyStatusUseCase(userWalletId, initialCurrencyFrom).getOrNull()
val toAccountStatus = initialCurrencyTo?.let { currencyTo ->
getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = userWalletId,
currency = currencyTo,
).getOrNull()
}
val toPaymentAccountStatus = initialCurrencyTo?.let { currencyTo ->
paymentAccountCryptoCurrencyStatusUseCase(userWalletId, currencyTo).getOrNull()
}
val fromAccount = fromAccountStatus?.account ?: fromPaymentAccountStatus?.first
val fromStatus = fromAccountStatus?.status ?: fromPaymentAccountStatus?.second
if (fromAccountStatus == null) {
if (fromAccount != null && fromStatus != null) {
this@SwapModel.fromAccount = fromAccount
this@SwapModel.fromAccountStatus = fromStatus
this@SwapModel.toAccount = toAccountStatus?.account ?: toPaymentAccountStatus?.first
this@SwapModel.toAccountStatus = toAccountStatus?.status ?: toPaymentAccountStatus?.second
this@SwapModel.initialFromStatus = fromStatus
this@SwapModel.initialToStatus = toAccountStatus?.status ?: toPaymentAccountStatus?.second
initTokens(isInitiallyReversed)
} else {
showAlert()
swapRouter.back()
} else {
fromAccountCurrencyStatus = fromAccountStatus
toAccountCurrencyStatus = toAccountStatus
initialFromStatus = fromAccountStatus.status
initialToStatus = toAccountStatus?.status
initTokens(isInitiallyReversed)
}
} else {
val fromStatus = getFromStatus()
@ -412,7 +429,7 @@ internal class SwapModel @Inject constructor(
updateTokensState(state)
val (selectedCurrency, selectedAccount) = run {
var selectedAccountCurrency = toAccountCurrencyStatus
var selectedAccountCurrency = toAccountStatus
if (selectedAccountCurrency == null) {
val amountSwapCurrency = swapInteractor.getInitialCurrencyToSwap(
@ -422,14 +439,11 @@ internal class SwapModel @Inject constructor(
)
if (amountSwapCurrency != null) {
selectedAccountCurrency = AccountCryptoCurrencyStatus(
account = amountSwapCurrency.account,
status = amountSwapCurrency.cryptoCurrencyStatus,
)
selectedAccountCurrency = amountSwapCurrency.cryptoCurrencyStatus
}
}
selectedAccountCurrency?.status to selectedAccountCurrency?.account
selectedAccountCurrency to toAccount
}
val isApplied = applyInitialTokenChoice(
@ -541,7 +555,7 @@ internal class SwapModel @Inject constructor(
private fun applyInitialTokenChoice(
state: TokensDataStateExpress,
selectedCurrency: CryptoCurrencyStatus?,
selectedAccount: Account.CryptoPortfolio?,
selectedAccount: Account?,
isReverseFromTo: Boolean,
): Boolean {
// exceptional case
@ -562,9 +576,9 @@ internal class SwapModel @Inject constructor(
}
val (fromAccount, toAccount) = if (canUseFromAccountCurrencyStatus) {
if (isOrderReversed.value) {
selectedAccount to requireNotNull(fromAccountCurrencyStatus).account
selectedAccount to fromAccount
} else {
requireNotNull(fromAccountCurrencyStatus).account to selectedAccount
fromAccount to selectedAccount
}
} else {
null to null
@ -607,9 +621,9 @@ internal class SwapModel @Inject constructor(
private fun startLoadingQuotes(
fromToken: CryptoCurrencyStatus,
fromAccount: Account.CryptoPortfolio?,
fromAccount: Account?,
toToken: CryptoCurrencyStatus,
toAccount: Account.CryptoPortfolio?,
toAccount: Account?,
amount: String,
reduceBalanceBy: BigDecimal,
toProvidersList: List<SwapProvider>,
@ -680,9 +694,9 @@ internal class SwapModel @Inject constructor(
private fun loadQuotesTask(
fromToken: CryptoCurrencyStatus,
fromAccount: Account.CryptoPortfolio?,
fromAccount: Account?,
toToken: CryptoCurrencyStatus,
toAccount: Account.CryptoPortfolio?,
toAccount: Account?,
amount: String,
reduceBalanceBy: BigDecimal,
toProvidersList: List<SwapProvider>,
@ -784,7 +798,7 @@ internal class SwapModel @Inject constructor(
selectedFeeType = (getSelectedFee() as? TxFee.Legacy)?.feeType ?: FeeType.NORMAL,
isReverseSwapPossible = isReverseSwapPossible(),
needApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
hideFee = tangemPayInput?.isWithdrawal == true,
hideFee = isTangemPayWithdrawal(),
)
}
@ -873,11 +887,11 @@ internal class SwapModel @Inject constructor(
private fun sendErrorAnalyticsEvent(error: ExpressDataError, provider: SwapProvider) {
val receiveToken = dataState.toCryptoCurrency?.currency?.let { currency ->
"${currency.network.backendId}:${currency.symbol}"
"${currency.network.rawId}:${currency.symbol}"
}
analyticsErrorEventHandler.sendErrorEvent(
SwapEvents.NoticeProviderError(
sendToken = "${initialCurrencyFrom.network.backendId}:${initialCurrencyFrom.symbol}",
sendToken = "${initialCurrencyFrom.network.rawId}:${initialCurrencyFrom.symbol}",
receiveToken = receiveToken.orEmpty(),
provider = provider,
errorCode = error.code,
@ -978,8 +992,9 @@ internal class SwapModel @Inject constructor(
}
val fromCurrency = requireNotNull(dataState.fromCryptoCurrency)
val fee = getSelectedFee()
val isTangemPayWithdrawal = isTangemPayWithdrawal()
if (fee == null && tangemPayInput?.isWithdrawal != true) {
if (fee == null && !isTangemPayWithdrawal) {
TangemLogger.e("onSwapClick: fee is null and isWithdrawal is ${tangemPayInput?.isWithdrawal}")
showAlert(resourceReference(R.string.swapping_fee_estimation_error_text))
modelScope.launch {
@ -1001,7 +1016,7 @@ internal class SwapModel @Inject constructor(
includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount,
fee = fee,
expressOperationType = ExpressOperationType.SWAP,
isTangemPayWithdrawal = tangemPayInput?.isWithdrawal == true,
isTangemPayWithdrawal = isTangemPayWithdrawal,
)
}.onSuccess { swapTransactionState ->
when (swapTransactionState) {
@ -1166,7 +1181,7 @@ internal class SwapModel @Inject constructor(
}
runCatching(dispatchers.io) {
swapInteractor.givePermissionToSwap(
networkId = fromToken.network.backendId,
networkId = fromToken.network.rawId,
permissionOptions = PermissionOptions(
approveData = approveDataModel,
forTokenContractAddress = (fromToken as? CryptoCurrency.Token)?.contractAddress.orEmpty(),
@ -1259,11 +1274,12 @@ internal class SwapModel @Inject constructor(
}
@Suppress("LongMethod")
private fun onTokenSelect(id: String, isSearched: Boolean) {
private fun onTokenSelect(account: Account, cryptoCurrencyStatus: CryptoCurrencyStatus, isSearched: Boolean) {
val tokens = dataState.tokensDataState ?: return
val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id)
val foundToken = cryptoCurrencyStatus
val foundAccount = account
foundToken?.currency?.symbol?.let { symbol ->
foundToken.currency.symbol.let { symbol ->
analyticsEventHandler.send(
SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol),
)
@ -1277,108 +1293,90 @@ internal class SwapModel @Inject constructor(
)
}
if (foundToken != null) {
val fromToken: CryptoCurrencyStatus
val fromAccount: Account.CryptoPortfolio?
val toToken: CryptoCurrencyStatus
val toAccount: Account.CryptoPortfolio?
if (isOrderReversed.value) {
fromToken = foundToken
fromAccount = foundAccount
toToken = initialFromStatus
toAccount = fromAccountCurrencyStatus?.account
val fromToken: CryptoCurrencyStatus
val fromAccount: Account?
val toToken: CryptoCurrencyStatus
val toAccount: Account?
if (isOrderReversed.value) {
fromToken = foundToken
fromAccount = foundAccount
toToken = initialFromStatus
toAccount = this.fromAccount
val newToken = fromToken.currency as? CryptoCurrency.Coin
if (newToken != null) {
subscribeToCoinBalanceUpdates(
userWalletId = userWalletId,
coin = newToken,
isFromCurrency = true,
)
} else {
fromTokenBalanceJobHolder.cancel()
}
val newToken = fromToken.currency as? CryptoCurrency.Coin
if (newToken != null) {
subscribeToCoinBalanceUpdates(
userWalletId = userWalletId,
coin = newToken,
isFromCurrency = true,
)
} else {
fromToken = initialFromStatus
fromAccount = fromAccountCurrencyStatus?.account
toToken = foundToken
toAccount = foundAccount
val newToken = toToken.currency as? CryptoCurrency.Coin
if (newToken != null) {
subscribeToCoinBalanceUpdates(
userWalletId = userWalletId,
coin = newToken,
isFromCurrency = false,
)
} else {
toTokenBalanceJobHolder.cancel()
}
fromTokenBalanceJobHolder.cancel()
}
if (dataState.fromCryptoCurrency != null && dataState.tokensDataState != null) {
isAmountChangedByUser = true
}
dataState = dataState.copy(
fromCryptoCurrency = fromToken,
fromAccount = fromAccount,
toCryptoCurrency = toToken,
toAccount = toAccount,
selectedProvider = null,
)
swapRouter.openScreen(SwapNavScreen.Main)
if (handleSwapNotSupported(
state = tokens,
fromToken = fromToken,
toToken = toToken,
fromAccount = fromAccount,
toAccount = toAccount,
)
) {
return
}
modelScope.launch {
TangemLogger.i(
"updateFeePaidCryptoCurrencyFor: id = ${fromToken.currency.id}, " +
"isOrderReversed: ${isOrderReversed.value}",
)
if ((uiState.sendCardData as? SwapCardState.SwapCardData)?.type is TransactionCardType.ReadOnly) {
uiState = stateBuilder.createInitialLoadingState(
initialCurrencyFrom = fromToken.currency,
initialCurrencyTo = toToken.currency,
fromNetworkInfo = fromToken.currency.getNetworkInfo(),
)
}
updateFeePaidCryptoCurrencyFor(fromToken)
startLoadingQuotes(
fromToken = fromToken,
fromAccount = fromAccount,
toToken = toToken,
toAccount = toAccount,
amount = lastAmount.value,
reduceBalanceBy = lastReducedBalanceBy.value,
toProvidersList = findSwapProviders(fromToken, toToken),
)
}
updateTokensState(tokens)
}
}
private fun getSelectedTokenAndAccount(
tokens: TokensDataStateExpress,
id: String,
): Pair<CryptoCurrencyStatus?, Account.CryptoPortfolio?> {
val accountCryptoCurrencyStatus = if (isOrderReversed.value) {
tokens.fromGroup
} else {
tokens.toGroup
}.accountCurrencyList.firstNotNullOfOrNull { accountSwapAvailability ->
accountSwapAvailability.currencyList.firstOrNull { accountSwapCurrency ->
accountSwapCurrency.cryptoCurrencyStatus.currency.id.value == id
fromToken = initialFromStatus
fromAccount = this.fromAccount
toToken = foundToken
toAccount = foundAccount
val newToken = toToken.currency as? CryptoCurrency.Coin
if (newToken != null) {
subscribeToCoinBalanceUpdates(
userWalletId = userWalletId,
coin = newToken,
isFromCurrency = false,
)
} else {
toTokenBalanceJobHolder.cancel()
}
}
return accountCryptoCurrencyStatus?.cryptoCurrencyStatus to accountCryptoCurrencyStatus?.account
if (dataState.fromCryptoCurrency != null && dataState.tokensDataState != null) {
isAmountChangedByUser = true
}
dataState = dataState.copy(
fromCryptoCurrency = fromToken,
fromAccount = fromAccount,
toCryptoCurrency = toToken,
toAccount = toAccount,
selectedProvider = null,
)
swapRouter.openScreen(SwapNavScreen.Main)
if (handleSwapNotSupported(
state = tokens,
fromToken = fromToken,
toToken = toToken,
fromAccount = fromAccount,
toAccount = toAccount,
)
) {
return
}
modelScope.launch {
TangemLogger.i(
"updateFeePaidCryptoCurrencyFor: id = ${fromToken.currency.id}, " +
"isOrderReversed: ${isOrderReversed.value}",
)
if ((uiState.sendCardData as? SwapCardState.SwapCardData)?.type is TransactionCardType.ReadOnly) {
uiState = stateBuilder.createInitialLoadingState(
initialCurrencyFrom = fromToken.currency,
initialCurrencyTo = toToken.currency,
fromNetworkInfo = fromToken.currency.getNetworkInfo(),
)
}
updateFeePaidCryptoCurrencyFor(fromToken)
startLoadingQuotes(
fromToken = fromToken,
fromAccount = fromAccount,
toToken = toToken,
toAccount = toAccount,
amount = lastAmount.value,
reduceBalanceBy = lastReducedBalanceBy.value,
toProvidersList = findSwapProviders(fromToken, toToken),
)
}
updateTokensState(tokens)
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
@ -1930,8 +1928,8 @@ internal class SwapModel @Inject constructor(
state: TokensDataStateExpress,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
fromAccount: Account.CryptoPortfolio?,
toAccount: Account.CryptoPortfolio?,
fromAccount: Account?,
toAccount: Account?,
): Boolean {
val selectedCurrency = if (isOrderReversed.value) fromToken else toToken
if (isTokenAvailableForSwap(state, selectedCurrency, isOrderReversed.value)) return false
@ -1979,8 +1977,12 @@ internal class SwapModel @Inject constructor(
}
}
private fun isTangemPayWithdrawal(): Boolean {
return tangemPayInput?.isWithdrawal == true || dataState.fromAccount is Account.Payment
}
private fun List<SwapProvider>.filterForTangemPayWithdrawal(): List<SwapProvider> {
return if (tangemPayInput?.isWithdrawal == true) {
return if (isTangemPayWithdrawal()) {
filter { it.type == ExchangeProviderType.CEX }
} else {
this
@ -2117,8 +2119,7 @@ internal class SwapModel @Inject constructor(
saveBlockchainErrorUseCase(
error = BlockchainErrorInfo(
errorMessage = errorMessage,
blockchainId = network.rawId,
derivationPath = network.derivationPath.value,
networkId = network.id,
destinationAddress = transaction?.txTo.orEmpty(),
tokenSymbol = fromCurrencyStatus.currency.symbol,
amount = dataState.amount.orEmpty(),

View file

@ -15,8 +15,8 @@ data class SwapProcessDataState(
val fromCryptoCurrency: CryptoCurrencyStatus? = null,
val toCryptoCurrency: CryptoCurrencyStatus? = null,
val feePaidCryptoCurrency: CryptoCurrencyStatus? = null,
val fromAccount: Account.CryptoPortfolio? = null,
val toAccount: Account.CryptoPortfolio? = null,
val fromAccount: Account? = null,
val toAccount: Account? = null,
// Amount from input
val amount: String? = null,
val reduceBalanceBy: BigDecimal = BigDecimal.ZERO,

View file

@ -3,6 +3,7 @@ package com.tangem.feature.swap.ui
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.ui.account.AccountIconUM
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.toUM
@ -89,7 +90,7 @@ internal class StateBuilder(
token = null,
tokenIconUrl = initialCurrencyFrom.iconUrl,
tokenCurrency = initialCurrencyFrom.symbol,
coinId = initialCurrencyFrom.network.backendId,
coinId = initialCurrencyFrom.network.rawId,
canSelectAnotherToken = false,
isNotNativeToken = initialCurrencyFrom is CryptoCurrency.Token,
balance = "",
@ -107,7 +108,7 @@ internal class StateBuilder(
balance = "",
isNotNativeToken = initialCurrencyTo is CryptoCurrency.Token,
networkIconRes = initialCurrencyTo?.let { getActiveIconRes(it.network.rawId) },
coinId = initialCurrencyTo?.network?.backendId,
coinId = initialCurrencyTo?.network?.rawId,
isBalanceHidden = true,
),
fee = FeeItemState.Empty,
@ -144,7 +145,7 @@ internal class StateBuilder(
amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
token = fromToken,
tokenIconUrl = fromToken.currency.iconUrl,
coinId = fromToken.currency.network.backendId,
coinId = fromToken.currency.network.rawId,
isNotNativeToken = fromToken.currency is CryptoCurrency.Token,
tokenCurrency = fromToken.currency.symbol,
canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken,
@ -177,8 +178,8 @@ internal class StateBuilder(
uiStateHolder: SwapStateHolder,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
fromAccount: Account.CryptoPortfolio?,
toAccount: Account.CryptoPortfolio?,
fromAccount: Account?,
toAccount: Account?,
mainTokenId: String,
): SwapStateHolder {
val canSelectSendToken = mainTokenId != fromToken.currency.id.value
@ -195,7 +196,7 @@ internal class StateBuilder(
amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
token = fromToken,
tokenIconUrl = fromToken.currency.iconUrl,
coinId = fromToken.currency.network.backendId,
coinId = fromToken.currency.network.rawId,
isNotNativeToken = fromToken.currency is CryptoCurrency.Token,
tokenCurrency = fromToken.currency.symbol,
canSelectAnotherToken = canSelectSendToken,
@ -213,7 +214,7 @@ internal class StateBuilder(
amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
token = toToken,
tokenIconUrl = toToken.currency.iconUrl,
coinId = toToken.currency.network.backendId,
coinId = toToken.currency.network.rawId,
isNotNativeToken = toToken.currency is CryptoCurrency.Token,
tokenCurrency = toToken.currency.symbol,
canSelectAnotherToken = canSelectReceiveToken,
@ -241,8 +242,8 @@ internal class StateBuilder(
fromToken: CryptoCurrency,
toToken: CryptoCurrency,
mainTokenId: String,
fromAccount: Account.CryptoPortfolio?,
toAccount: Account.CryptoPortfolio?,
fromAccount: Account?,
toAccount: Account?,
): SwapStateHolder {
val canSelectSendToken = mainTokenId != fromToken.id.value
val canSelectReceiveToken = mainTokenId != toToken.id.value
@ -265,7 +266,7 @@ internal class StateBuilder(
token = uiStateHolder.sendCardData.token,
tokenIconUrl = fromToken.iconUrl,
tokenCurrency = fromToken.symbol,
coinId = fromToken.network.backendId,
coinId = fromToken.network.rawId,
isNotNativeToken = fromToken is CryptoCurrency.Token,
canSelectAnotherToken = canSelectSendToken,
balance = if (!canSelectSendToken) uiStateHolder.sendCardData.balance else "",
@ -281,7 +282,7 @@ internal class StateBuilder(
token = uiStateHolder.receiveCardData.token,
tokenIconUrl = toToken.iconUrl,
tokenCurrency = toToken.symbol,
coinId = toToken.network.backendId,
coinId = toToken.network.rawId,
isNotNativeToken = toToken is CryptoCurrency.Token,
canSelectAnotherToken = canSelectReceiveToken,
balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "",
@ -373,7 +374,7 @@ internal class StateBuilder(
amountEquivalent = getFormattedFiatAmount(quoteModel.fromTokenInfo.amountFiat),
token = fromCurrencyStatus,
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
coinId = fromCurrencyStatus.currency.network.backendId,
coinId = fromCurrencyStatus.currency.network.rawId,
isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken,
tokenCurrency = uiStateHolder.sendCardData.tokenCurrency,
canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken,
@ -414,7 +415,7 @@ internal class StateBuilder(
},
token = toCurrencyStatus,
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
coinId = toCurrencyStatus.currency.network.backendId,
coinId = toCurrencyStatus.currency.network.rawId,
isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken,
tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency,
canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken,
@ -501,7 +502,7 @@ internal class StateBuilder(
swapProvider: SwapProvider,
fromToken: TokenSwapInfo,
toToken: CryptoCurrencyStatus?,
toAccount: Account.CryptoPortfolio?,
toAccount: Account?,
includeFeeInAmount: IncludeFeeInAmount,
expressDataError: ExpressDataError,
isReverseSwapPossible: Boolean,
@ -534,7 +535,7 @@ internal class StateBuilder(
amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
token = toToken,
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
coinId = toToken.currency.network.backendId,
coinId = toToken.currency.network.rawId,
isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken,
tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency,
canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken,
@ -618,7 +619,7 @@ internal class StateBuilder(
emptyAmountState: SwapState.EmptyAmountState,
fromTokenStatus: CryptoCurrencyStatus,
toTokenStatus: CryptoCurrencyStatus?,
toAccount: Account.CryptoPortfolio?,
toAccount: Account?,
isReverseSwapPossible: Boolean,
): SwapStateHolder {
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
@ -693,7 +694,7 @@ internal class StateBuilder(
amountFormatted: String,
amountRaw: String,
fromToken: CryptoCurrency,
fromAccount: Account.CryptoPortfolio?,
fromAccount: Account?,
minTxAmount: BigDecimal?,
): SwapStateHolder {
if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState
@ -1327,30 +1328,37 @@ internal class StateBuilder(
return FCA_RESTRICTED_PROVIDER_IDS.contains(providerId)
}
private fun getFromCardAccountTitle(fromAccount: Account.CryptoPortfolio?): AccountTitleUM {
private fun getFromCardAccountTitle(fromAccount: Account?): AccountTitleUM {
return if (fromAccount != null && isAccountsModeProvider()) {
AccountTitleUM.Account(
prefixText = resourceReference(R.string.common_from),
name = fromAccount.accountName.toUM().value,
icon = CryptoPortfolioIconConverter.convert(fromAccount.icon),
icon = fromAccount.toIconUM(),
)
} else {
AccountTitleUM.Text(resourceReference(R.string.swapping_from_title))
}
}
private fun getToCardAccountTitle(toAccount: Account.CryptoPortfolio?): AccountTitleUM {
private fun getToCardAccountTitle(toAccount: Account?): AccountTitleUM {
return if (toAccount != null && isAccountsModeProvider()) {
AccountTitleUM.Account(
prefixText = resourceReference(R.string.common_to),
name = toAccount.accountName.toUM().value,
icon = CryptoPortfolioIconConverter.convert(toAccount.icon),
icon = toAccount.toIconUM(),
)
} else {
AccountTitleUM.Text(resourceReference(R.string.swapping_to_title))
}
}
private fun Account.toIconUM(): AccountIconUM {
return when (this) {
is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(icon)
is Account.Payment -> AccountIconUM.Payment
}
}
private fun getChangeCardsButtonState(isReverseSwapPossible: Boolean) = if (isReverseSwapPossible) {
ChangeCardsButtonState.ENABLED
} else {