Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-24 15:20:12 +05:00
parent 20682ca353
commit d863d37629
13 changed files with 437 additions and 57 deletions

View file

@ -0,0 +1,147 @@
package com.tangem.feature.swap.converters
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedCryptoAmount
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedFiatAmount
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering
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.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.feature.swap.domain.models.ui.AccountSwapAvailability
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toPersistentList
internal class AccountTokenItemConverter(
private val appCurrency: AppCurrency,
private val unavailableErrorText: TextReference,
private val onItemClick: (String) -> Unit,
) : Converter<AccountSwapAvailability, TokensListItemUM.Portfolio> {
override fun convert(value: AccountSwapAvailability): TokensListItemUM.Portfolio {
return TokensListItemUM.Portfolio(
tokenItemUM = AccountCryptoPortfolioItemStateConverter(
appCurrency = appCurrency,
account = value.account,
onItemClick = null,
).convert(TotalFiatBalance.Failed),
isExpanded = true,
isCollapsable = false,
tokens = value.currencyList.map { accountSwapCurrency ->
if (accountSwapCurrency.isAvailable) {
createAvailableItemConverter()
} else {
createUnavailableItemConverter()
}.convert(accountSwapCurrency.cryptoCurrencyStatus)
}.map(TokensListItemUM::Token).toPersistentList(),
)
}
fun createAvailableItemConverter(): TokenItemStateConverter {
return TokenItemStateConverter(
appCurrency = appCurrency,
subtitleStateProvider = {
createSubtitleState(
status = it,
isAvailable = true,
text = stringReference(value = it.currency.symbol),
)
},
subtitle2StateProvider = ::createSubtitle2State,
fiatAmountStateProvider = {
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = true)
},
onItemClick = { account, currencyStatus -> onItemClick(currencyStatus.currency.id.value) },
)
}
fun createUnavailableItemConverter(): TokenItemStateConverter {
return TokenItemStateConverter(
appCurrency = appCurrency,
iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) },
titleStateProvider = {
TokenItemState.TitleState.Content(
text = stringReference(value = it.currency.name),
isAvailable = false,
)
},
subtitleStateProvider = {
createSubtitleState(
status = it,
isAvailable = false,
text = unavailableErrorText,
)
},
subtitle2StateProvider = ::createSubtitle2State,
fiatAmountStateProvider = {
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = false)
},
)
}
private fun createSubtitleState(
status: CryptoCurrencyStatus,
isAvailable: Boolean,
text: TextReference,
): TokenItemState.SubtitleState {
return when (status.value) {
CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading
else -> {
TokenItemState.SubtitleState.TextContent(
value = text,
isAvailable = isAvailable,
)
}
}
}
private fun createSubtitle2State(status: CryptoCurrencyStatus): TokenItemState.Subtitle2State? {
return when (status.value) {
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> {
TokenItemState.Subtitle2State.TextContent(
text = status.getFormattedCryptoAmount(),
isFlickering = status.value.isFlickering(),
)
}
is CryptoCurrencyStatus.Loading,
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> null
}
}
private fun createFiatAmountStateProvider(
status: CryptoCurrencyStatus,
appCurrency: AppCurrency,
isAvailable: Boolean,
): TokenItemState.FiatAmountState? {
return when (status.value) {
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> {
TokenItemState.FiatAmountState.TextContent(
text = status.getFormattedFiatAmount(appCurrency = appCurrency),
isAvailable = isAvailable,
isFlickering = status.value.isFlickering(),
)
}
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Loading,
-> null
}
}
}

View file

@ -57,6 +57,7 @@ internal class TokensDataConverter(
tokensListData = TokenListUMData.EmptyList,
onSearchEntered = onSearchEntered,
onTokenSelected = onTokenSelected,
isBalanceHidden = isBalanceHiddenProvider(),
afterSearch = group.isAfterSearch,
)
}

View file

@ -0,0 +1,67 @@
package com.tangem.feature.swap.converters
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.TokenListUMData
import com.tangem.feature.swap.presentation.R
import com.tangem.utils.Provider
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.plus
import kotlinx.collections.immutable.toPersistentList
internal class TokensDataConverterV2(
private val onSearchEntered: (String) -> Unit,
private val onTokenSelected: (String) -> Unit,
private val tokensDataState: CurrenciesGroup,
private val isBalanceHidden: Boolean,
private val isAccountsMode: Boolean,
appCurrencyProvider: Provider<AppCurrency>,
) : Transformer<SwapStateHolder> {
private val accountListItemConverter = AccountTokenItemConverter(
appCurrency = appCurrencyProvider(),
unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header),
onItemClick = onTokenSelected,
)
override fun transform(prevState: SwapStateHolder): SwapStateHolder {
val accountList = tokensDataState.accountCurrencyList
return prevState.copy(
selectTokenState = SwapSelectTokenStateHolder(
availableTokens = persistentListOf(),
unavailableTokens = persistentListOf(),
tokensListData = if (isAccountsMode) {
TokenListUMData.AccountList(
tokensList = accountListItemConverter.convertList(accountList).toPersistentList(),
)
} else {
TokenListUMData.TokenList(
tokensList = persistentListOf(
TokensListItemUM.GroupTitle(
id = "available_tokens_title",
text = resourceReference(R.string.exchange_tokens_available_tokens_header),
),
) + accountList.flatMap { (_, currencyList) ->
currencyList.asSequence().map { accountSwapCurrency ->
if (accountSwapCurrency.isAvailable) {
accountListItemConverter.createAvailableItemConverter()
} else {
accountListItemConverter.createUnavailableItemConverter()
}.convert(accountSwapCurrency.cryptoCurrencyStatus)
}.map(TokensListItemUM::Token).toPersistentList()
}.toPersistentList(),
)
},
onSearchEntered = onSearchEntered,
onTokenSelected = onTokenSelected,
isBalanceHidden = isBalanceHidden,
afterSearch = tokensDataState.isAfterSearch,
),
)
}
}

View file

@ -141,6 +141,7 @@ internal class SwapModel @Inject constructor(
actions = createUiActions(),
isBalanceHiddenProvider = Provider { isBalanceHidden },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
isAccountsModeProvider = Provider { isAccountsMode },
)
private val inputNumberFormatter =
@ -415,12 +416,11 @@ internal class SwapModel @Inject constructor(
val tokensDataState = if (isOrderReversed) tokenDataState.fromGroup else tokenDataState.toGroup
uiState = if (accountsFeatureToggles.isFeatureEnabled) {
// stateBuilder.addTokensToStateV2(
// uiState = uiState,
// tokensDataState = tokensDataState,
// isAccountsMode = isAccountsMode,
// )
TODO()
stateBuilder.addTokensToStateV2(
uiState = uiState,
tokensDataState = tokensDataState,
isAccountsMode = isAccountsMode,
)
} else {
stateBuilder.addTokensToState(
uiState = uiState,
@ -446,6 +446,8 @@ internal class SwapModel @Inject constructor(
uiStateHolder = uiState,
fromToken = fromToken.currency,
toToken = toToken.currency,
fromAccount = fromAccount,
toAccount = toAccount,
mainTokenId = initialCurrencyFrom.id.value,
)
}
@ -571,6 +573,7 @@ internal class SwapModel @Inject constructor(
fromTokenStatus = fromToken,
toTokenStatus = toTokenStatus,
isReverseSwapPossible = isReverseSwapPossible(),
toAccount = dataState.toAccount,
)
}
is SwapState.SwapError -> {
@ -584,6 +587,7 @@ internal class SwapModel @Inject constructor(
includeFeeInAmount = state.includeFeeInAmount,
isReverseSwapPossible = isReverseSwapPossible(),
needApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
toAccount = dataState.toAccount,
)
sendErrorAnalyticsEvent(state.error, provider)
}
@ -1120,6 +1124,7 @@ internal class SwapModel @Inject constructor(
amountRaw = lastAmount.value,
fromToken = newFromToken.currency,
minTxAmount = minTxAmount,
fromAccount = dataState.fromAccount,
)
startLoadingQuotes(
fromToken = newFromToken,
@ -1157,6 +1162,7 @@ internal class SwapModel @Inject constructor(
amountRaw = lastAmount.value,
fromToken = fromToken.currency,
minTxAmount = minTxAmount,
fromAccount = dataState.fromAccount,
)
if (toToken != null) {

View file

@ -10,6 +10,7 @@ internal data class SwapSelectTokenStateHolder(
val availableTokens: ImmutableList<TokenToSelectState>,
val unavailableTokens: ImmutableList<TokenToSelectState>,
val tokensListData: TokenListUMData,
val isBalanceHidden: Boolean,
val afterSearch: Boolean,
val onSearchEntered: (String) -> Unit,
val onTokenSelected: (String) -> Unit,

View file

@ -2,10 +2,10 @@ package com.tangem.feature.swap.models
import androidx.annotation.DrawableRes
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.common.ui.swapStoriesScreen.SwapStoriesUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.event.consumedEvent
@ -83,21 +83,21 @@ data class SwapButton(
sealed interface TransactionCardType {
val header: TextReference
val accountTitleUM: AccountTitleUM?
val inputError: InputError
data class Inputtable(
val onAmountChanged: ((String) -> Unit),
val onFocusChanged: ((Boolean) -> Unit),
override val inputError: InputError,
override val header: TextReference = TextReference.Res(R.string.swapping_from_title),
override val accountTitleUM: AccountTitleUM?,
) : TransactionCardType
data class ReadOnly(
val showWarning: Boolean = false,
val onWarningClick: (() -> Unit)? = null,
override val inputError: InputError = InputError.Empty,
override val header: TextReference = TextReference.Res(R.string.swapping_to_title),
override val accountTitleUM: AccountTitleUM? = null,
) : TransactionCardType
sealed interface InputError {

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.models
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
@ -12,6 +13,8 @@ data class SwapSuccessStateHolder(
val providerName: TextReference,
val providerType: TextReference,
val providerIcon: String,
val fromTitle: AccountTitleUM,
val toTitle: AccountTitleUM,
val fromTokenAmount: TextReference,
val toTokenAmount: TextReference,
val fromTokenFiatAmount: TextReference,

View file

@ -1,7 +1,12 @@
package com.tangem.feature.swap.preview
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
import com.tangem.feature.swap.models.SwapSuccessStateHolder
@ -14,6 +19,16 @@ internal data object SwapSuccessStatePreview {
providerType = TextReference.Str(ExchangeProviderType.DEX.providerName),
showStatusButton = false,
providerIcon = "",
fromTitle = AccountTitleUM.Account(
prefixText = stringReference("From"),
name = AccountNameUM.DefaultMain.value,
icon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM(),
),
toTitle = AccountTitleUM.Account(
prefixText = stringReference("To"),
name = AccountNameUM.DefaultMain.value,
icon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM(),
),
fromTokenAmount = TextReference.Str("1 000 DAI"),
toTokenAmount = TextReference.Str("1 000 MATIC"),
fromTokenFiatAmount = TextReference.Str("1 000 $"),

View file

@ -2,6 +2,8 @@ package com.tangem.feature.swap.ui
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
import com.tangem.common.ui.bottomsheet.permission.state.*
import com.tangem.common.ui.notifications.NotificationUM
@ -18,12 +20,14 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
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.UserWallet
import com.tangem.domain.promo.models.StoryContent
import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter
import com.tangem.feature.swap.converters.TokensDataConverter
import com.tangem.feature.swap.converters.TokensDataConverterV2
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
@ -59,6 +63,7 @@ internal class StateBuilder(
private val actions: UiActions,
private val isBalanceHiddenProvider: Provider<Boolean>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val isAccountsModeProvider: Provider<Boolean>,
) {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
@ -86,6 +91,7 @@ internal class StateBuilder(
onAmountChanged = actions.onAmountChanged,
onFocusChanged = actions.onAmountSelected,
inputError = TransactionCardType.InputError.Empty,
accountTitleUM = null,
),
amountEquivalent = null,
amountTextFieldValue = null,
@ -183,11 +189,14 @@ internal class StateBuilder(
)
}
@Suppress("LongParameterList")
fun createQuotesLoadingState(
uiStateHolder: SwapStateHolder,
fromToken: CryptoCurrency,
toToken: CryptoCurrency,
mainTokenId: String,
fromAccount: Account.CryptoPortfolio?,
toAccount: Account.CryptoPortfolio?,
): SwapStateHolder {
val canSelectSendToken = mainTokenId != fromToken.id.value
val canSelectReceiveToken = mainTokenId != toToken.id.value
@ -199,7 +208,7 @@ internal class StateBuilder(
} else {
sendInputType.copy(
inputError = TransactionCardType.InputError.Empty,
header = TextReference.Res(R.string.swapping_from_title),
accountTitleUM = getFromCardAccountTitle(fromAccount),
)
}
return uiStateHolder.copy(
@ -218,7 +227,9 @@ internal class StateBuilder(
isBalanceHidden = isBalanceHiddenProvider(),
),
receiveCardData = SwapCardState.SwapCardData(
type = TransactionCardType.ReadOnly(),
type = TransactionCardType.ReadOnly(
accountTitleUM = getToCardAccountTitle(toAccount),
),
amountTextFieldValue = null,
amountEquivalent = null,
token = uiStateHolder.receiveCardData.token,
@ -280,10 +291,12 @@ internal class StateBuilder(
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
val isInsufficientFunds = isInsufficientFundsCondition(quoteModel)
val insufficientFundsHeader = if (isInsufficientFunds) {
TextReference.Res(R.string.swapping_insufficient_funds)
} else {
TextReference.Res(R.string.swapping_from_title)
val fromAccount = quoteModel.fromTokenInfo.account
val toAccount = quoteModel.toTokenInfo.account
val fromAccountTitleUM = when {
isInsufficientFunds -> AccountTitleUM.Text(TextReference.Res(R.string.swapping_insufficient_funds))
else -> getFromCardAccountTitle(fromAccount)
}
val sendCardType = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable)
val sendInput = when (sendCardType.inputError) {
@ -299,7 +312,7 @@ internal class StateBuilder(
}
sendCardType.copy(
inputError = error,
header = insufficientFundsHeader,
accountTitleUM = fromAccountTitleUM,
)
}
}
@ -321,7 +334,8 @@ internal class StateBuilder(
receiveCardData = SwapCardState.SwapCardData(
type = TransactionCardType.ReadOnly(
showWarning = true,
actions.onReceiveCardWarningClick,
onWarningClick = actions.onReceiveCardWarningClick,
accountTitleUM = getToCardAccountTitle(toAccount),
),
amountTextFieldValue = TextFieldValue(
quoteModel.toTokenInfo.tokenAmount
@ -423,6 +437,7 @@ internal class StateBuilder(
swapProvider: SwapProvider,
fromToken: TokenSwapInfo,
toToken: CryptoCurrencyStatus?,
toAccount: Account.CryptoPortfolio?,
includeFeeInAmount: IncludeFeeInAmount,
expressDataError: ExpressDataError,
isReverseSwapPossible: Boolean,
@ -445,9 +460,10 @@ internal class StateBuilder(
selectionType = ProviderState.SelectionType.CLICK,
needApplyFCARestrictions = needApplyFCARestrictions,
)
val type = TransactionCardType.ReadOnly(accountTitleUM = getToCardAccountTitle(toAccount))
val receiveCardData = toToken?.let {
SwapCardState.SwapCardData(
type = TransactionCardType.ReadOnly(),
type = type,
amountTextFieldValue = TextFieldValue(
text = "0",
),
@ -463,7 +479,7 @@ internal class StateBuilder(
isBalanceHidden = isBalanceHiddenProvider(),
)
} ?: SwapCardState.Empty(
type = TransactionCardType.ReadOnly(),
type = type,
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
amountTextFieldValue = TextFieldValue(
text = "0",
@ -535,11 +551,13 @@ internal class StateBuilder(
}
}
@Suppress("LongParameterList")
fun createQuotesEmptyAmountState(
uiStateHolder: SwapStateHolder,
emptyAmountState: SwapState.EmptyAmountState,
fromTokenStatus: CryptoCurrencyStatus,
toTokenStatus: CryptoCurrencyStatus?,
toAccount: Account.CryptoPortfolio?,
isReverseSwapPossible: Boolean,
): SwapStateHolder {
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
@ -560,7 +578,9 @@ internal class StateBuilder(
isBalanceHidden = isBalanceHiddenProvider(),
),
receiveCardData = SwapCardState.SwapCardData(
type = TransactionCardType.ReadOnly(),
type = TransactionCardType.ReadOnly(
accountTitleUM = getToCardAccountTitle(toAccount),
),
amountTextFieldValue = TextFieldValue("0"),
amountEquivalent = emptyAmountState.zeroAmountEquivalent,
token = uiStateHolder.receiveCardData.token,
@ -614,17 +634,34 @@ internal class StateBuilder(
)
}
fun addTokensToStateV2(
uiState: SwapStateHolder,
tokensDataState: CurrenciesGroup,
isAccountsMode: Boolean,
): SwapStateHolder {
return TokensDataConverterV2(
onSearchEntered = actions.onSearchEntered,
onTokenSelected = actions.onTokenSelected,
appCurrencyProvider = appCurrencyProvider,
tokensDataState = tokensDataState,
isBalanceHidden = isBalanceHiddenProvider(),
isAccountsMode = isAccountsMode,
).transform(uiState)
}
fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder {
return uiState.copy(
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
)
}
@Suppress("LongParameterList")
fun updateSwapAmount(
uiState: SwapStateHolder,
amountFormatted: String,
amountRaw: String,
fromToken: CryptoCurrency,
fromAccount: Account.CryptoPortfolio?,
minTxAmount: BigDecimal?,
): SwapStateHolder {
if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState
@ -635,12 +672,14 @@ internal class StateBuilder(
}
(uiState.sendCardData.type as? TransactionCardType.Inputtable)?.copy(
inputError = TransactionCardType.InputError.WrongAmount,
header = resourceReference(R.string.transfer_min_amount_error, wrappedList(minAmountFormatted)),
accountTitleUM = AccountTitleUM.Text(
resourceReference(R.string.transfer_min_amount_error, wrappedList(minAmountFormatted)),
),
) ?: uiState.sendCardData.type
} else {
(uiState.sendCardData.type as? TransactionCardType.Inputtable)?.copy(
inputError = TransactionCardType.InputError.Empty,
header = TextReference.Res(R.string.swapping_from_title),
accountTitleUM = getFromCardAccountTitle(fromAccount),
) ?: uiState.sendCardData.type
}
return uiState.copy(
@ -692,6 +731,7 @@ internal class StateBuilder(
isBalanceHidden = isBalanceHidden,
)
val selectTokenState = uiState.selectTokenState?.copy(
isBalanceHidden = isBalanceHidden,
availableTokens = uiState.selectTokenState.availableTokens.map {
when (it) {
is TokenToSelectState.TokenToSelect -> {
@ -811,6 +851,8 @@ internal class StateBuilder(
providerIcon = providerState.iconUrl,
rate = providerState.subtitle,
fee = stringReference("${fee.feeCryptoFormattedWithNative} (${fee.feeFiatFormattedWithNative})"),
fromTitle = getFromCardAccountTitle(fromAccount = dataState.fromAccount),
toTitle = getToCardAccountTitle(toAccount = dataState.toAccount),
fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()),
toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()),
fromTokenFiatAmount = stringReference(fromFiatAmount),
@ -1327,6 +1369,30 @@ internal class StateBuilder(
return FCA_RESTRICTED_PROVIDER_IDS.contains(providerId)
}
private fun getFromCardAccountTitle(fromAccount: Account.CryptoPortfolio?): AccountTitleUM {
return if (fromAccount != null && isAccountsModeProvider()) {
AccountTitleUM.Account(
prefixText = resourceReference(R.string.common_from),
name = fromAccount.accountName.toUM().value,
icon = fromAccount.icon.toUM(),
)
} else {
AccountTitleUM.Text(resourceReference(R.string.swapping_from_title))
}
}
private fun getToCardAccountTitle(toAccount: Account.CryptoPortfolio?): AccountTitleUM {
return if (toAccount != null && isAccountsModeProvider()) {
AccountTitleUM.Account(
prefixText = resourceReference(R.string.common_to),
name = toAccount.accountName.toUM().value,
icon = toAccount.icon.toUM(),
)
} else {
AccountTitleUM.Text(resourceReference(R.string.swapping_to_title))
}
}
private companion object {
private const val RU_LOCALE = "ru"
private const val EN_LOCALE = "en"

View file

@ -383,7 +383,12 @@ private fun MainButton(state: SwapStateHolder) {
// region preview
private val sendCard = SwapCardState.SwapCardData(
type = TransactionCardType.Inputtable({}, {}, TransactionCardType.InputError.Empty),
type = TransactionCardType.Inputtable(
onAmountChanged = {},
onFocusChanged = {},
inputError = TransactionCardType.InputError.Empty,
accountTitleUM = null,
),
amountTextFieldValue = TextFieldValue(),
amountEquivalent = "1 000 000",
tokenIconUrl = "",

View file

@ -146,13 +146,13 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier =
is TokenListUMData.AccountList -> list.tokensList.forEach { item ->
portfolioTokensList(
portfolio = item,
isBalanceHidden = false, // state.isBalanceHidden,
isBalanceHidden = state.isBalanceHidden,
)
}
is TokenListUMData.TokenList -> {
tokensList(
items = list.tokensList,
isBalanceHidden = false, // state.isBalanceHidden,
isBalanceHidden = state.isBalanceHidden,
)
}
TokenListUMData.EmptyList -> Unit
@ -418,6 +418,7 @@ private fun TokenScreenPreview() {
unavailableTokens = listOf(title, token, token, token).toImmutableList(),
tokensListData = TokenListUMData.EmptyList,
afterSearch = false,
isBalanceHidden = false,
onSearchEntered = {},
onTokenSelected = {},
),

View file

@ -5,21 +5,23 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountTitle
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.inputrow.InputRowBestRate
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.components.inputrow.InputRowImage
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.wrappedList
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.utils.DateTimeFormatters
@ -75,26 +77,18 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad
),
)
SpacerH16()
InputRowImage(
title = TextReference.Res(R.string.swapping_from_title),
SwapAmountBlock(
title = state.fromTitle,
subtitle = state.fromTokenAmount,
caption = state.fromTokenFiatAmount,
tokenIconState = state.fromTokenIconState ?: CurrencyIconState.Loading,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
showNetworkIcon = true,
tokenIconState = state.fromTokenIconState,
)
SpacerH16()
InputRowImage(
title = TextReference.Res(R.string.swapping_to_title),
SwapAmountBlock(
title = state.toTitle,
subtitle = state.toTokenAmount,
caption = state.toTokenFiatAmount,
tokenIconState = state.toTokenIconState ?: CurrencyIconState.Loading,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
showNetworkIcon = true,
tokenIconState = state.toTokenIconState,
)
SpacerH16()
InputRowBestRate(
@ -117,6 +111,48 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad
}
}
@Composable
private fun SwapAmountBlock(
title: AccountTitleUM,
subtitle: TextReference,
caption: TextReference,
tokenIconState: CurrencyIconState?,
) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.fillMaxWidth()
.padding(12.dp),
) {
AccountTitle(title)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
CurrencyIcon(
state = tokenIconState ?: CurrencyIconState.Loading,
shouldDisplayNetwork = true,
)
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = caption.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
}
@Suppress("LongParameterList")
@Composable
private fun SwapSuccessScreenButtons(

View file

@ -34,14 +34,20 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.common.ui.account.AccountTitle
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SwapTokenScreenTestTags
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.feature.swap.domain.models.ui.PriceImpact
import com.tangem.feature.swap.models.TransactionCardType
import kotlinx.coroutines.launch
@ -193,14 +199,22 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie
} else {
TangemTheme.colors.text.warning
}
Text(
text = type.header.resolveReference(),
color = titleColor,
maxLines = 1,
style = TangemTheme.typography.subtitle2,
modifier = Modifier
.align(Alignment.CenterVertically),
)
AnimatedContent(
targetState = type.accountTitleUM,
label = "",
) { currentAccountTitle ->
if (currentAccountTitle != null) {
AccountTitle(
accountTitleUM = currentAccountTitle,
textColor = titleColor,
)
} else {
TextShimmer(
text = stringResourceSafe(R.string.swapping_to_title),
style = TangemTheme.typography.subtitle2,
)
}
}
SpacerW16()
if (balance.isNotBlank()) {
AnimatedContent(targetState = balance, label = "") {
@ -559,7 +573,12 @@ private fun Preview_TransactionCardWithoutPriceImpact_InDarkTheme() {
@Composable
private fun TransactionCardPreview() {
TransactionCard(
type = TransactionCardType.Inputtable({}, {}, TransactionCardType.InputError.Empty),
type = TransactionCardType.Inputtable(
onAmountChanged = {},
onFocusChanged = {},
inputError = TransactionCardType.InputError.Empty,
accountTitleUM = null,
),
amountEquivalent = "1 000 000",
tokenIconUrl = "",
tokenCurrency = "DAI",
@ -575,7 +594,14 @@ private fun TransactionCardPreview() {
@Suppress("MagicNumber")
private fun TransactionCardPreviewWithPriceImpact() {
TransactionCard(
type = TransactionCardType.ReadOnly(showWarning = true),
type = TransactionCardType.ReadOnly(
showWarning = true,
accountTitleUM = AccountTitleUM.Account(
prefixText = resourceReference(R.string.common_from),
name = AccountNameUM.DefaultMain.value,
icon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM(),
),
),
amountEquivalent = "1 000 000",
tokenIconUrl = "",
tokenCurrency = "DAI",
@ -591,7 +617,13 @@ private fun TransactionCardPreviewWithPriceImpact() {
@Suppress("MagicNumber")
private fun TransactionCardPreviewWithoutPriceImpact() {
TransactionCard(
type = TransactionCardType.ReadOnly(),
type = TransactionCardType.ReadOnly(
accountTitleUM = AccountTitleUM.Account(
prefixText = resourceReference(R.string.common_from),
name = AccountNameUM.DefaultMain.value,
icon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM(),
),
),
amountEquivalent = "1 000 000",
tokenIconUrl = "",
tokenCurrency = "DAI",