From d863d3762975ea4264e415715b4177863d4b3aeb Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 24 Oct 2025 15:20:12 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../converters/AccountTokenItemConverter.kt | 147 ++++++++++++++++++ .../swap/converters/TokensDataConverter.kt | 1 + .../swap/converters/TokensDataConverterV2.kt | 67 ++++++++ .../tangem/feature/swap/model/SwapModel.kt | 18 ++- .../swap/models/SwapSelectTokenStateHolder.kt | 1 + .../feature/swap/models/SwapStateHolder.kt | 8 +- .../swap/models/SwapSuccessStateHolder.kt | 3 + .../swap/preview/SwapSuccessStatePreview.kt | 15 ++ .../tangem/feature/swap/ui/StateBuilder.kt | 92 +++++++++-- .../feature/swap/ui/SwapScreenContent.kt | 7 +- .../feature/swap/ui/SwapSelectTokenScreen.kt | 5 +- .../feature/swap/ui/SwapSuccessScreen.kt | 74 ++++++--- .../tangem/feature/swap/ui/TransactionCard.kt | 56 +++++-- 13 files changed, 437 insertions(+), 57 deletions(-) create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt new file mode 100644 index 0000000000..f556cdf5e5 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt @@ -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 { + + 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 + } + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index c4318d5226..e9f9ec7e68 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -57,6 +57,7 @@ internal class TokensDataConverter( tokensListData = TokenListUMData.EmptyList, onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, + isBalanceHidden = isBalanceHiddenProvider(), afterSearch = group.isAfterSearch, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt new file mode 100644 index 0000000000..3dbd07cda3 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt @@ -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, +) : Transformer { + + 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, + ), + ) + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 760015eb46..1fd58317e8 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -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) { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index 63fc478fc5..aab8a21eae 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -10,6 +10,7 @@ internal data class SwapSelectTokenStateHolder( val availableTokens: ImmutableList, val unavailableTokens: ImmutableList, val tokensListData: TokenListUMData, + val isBalanceHidden: Boolean, val afterSearch: Boolean, val onSearchEntered: (String) -> Unit, val onTokenSelected: (String) -> Unit, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 8b2076a065..37879acb33 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -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 { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt index 5d05e969b8..906782071d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt @@ -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, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt index 1be63f7e6f..9c3b0ea54d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt @@ -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 $"), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index a21003f5f4..8387c1b9e1 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -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, private val appCurrencyProvider: Provider, + private val isAccountsModeProvider: Provider, ) { 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" diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 2fe84bb466..5f6b7b2be3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -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 = "", diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index a21eb7613a..39625f9e1f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -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 = {}, ), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index 7e4d9649bf..ce74fd8b9a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -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( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 6443a3dd5f..a529440cd6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -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",