Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-05 17:49:05 +03:00
parent 54f13f1082
commit d5d03d73f2
12 changed files with 289 additions and 149 deletions

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.components.rows
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
@ -32,16 +33,20 @@ fun SimpleActionRow(title: String, description: String, modifier: Modifier = Mod
.align(Alignment.CenterStart),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
Text(
text = title,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
)
Text(
text = description,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
AnimatedContent(targetState = title, label = "") {
Text(
text = it,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
)
}
AnimatedContent(targetState = description, label = "") {
Text(
text = it,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
}
}
if (isClickable) {

View file

@ -99,7 +99,7 @@ internal class DefaultCurrenciesRepository(
)
storeAndPushTokens(
userWalletId = userWalletId,
response = updatedResponse
response = updatedResponse,
)
fetchExchangeableUserMarketCoinsByIds(userWalletId, updatedResponse)
}

View file

@ -64,6 +64,7 @@ interface SwapInteractor {
currencyToSend: CryptoCurrencyStatus,
currencyToGet: CryptoCurrencyStatus,
amountToSwap: String,
includeFeeInAmount: IncludeFeeInAmount,
fee: TxFee,
): TxState

View file

@ -294,7 +294,7 @@ internal class SwapInteractorImpl @Inject constructor(
transactionManager.updateWalletManager(networkId, derivationPath)
}
return if (isAllowedToSpend && isBalanceWithoutFeeEnough) {
provider to loadSwapData(
provider to loadDexSwapData(
provider = provider,
networkId = networkId,
fromToken = fromToken,
@ -312,8 +312,8 @@ internal class SwapInteractorImpl @Inject constructor(
networkId = networkId,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
providerType = provider.type,
selectedFee = selectedFee,
txFee = TxFeeState.Empty,
includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex
)
}
}
@ -327,7 +327,7 @@ internal class SwapInteractorImpl @Inject constructor(
isBalanceWithoutFeeEnough: Boolean,
selectedFee: FeeType,
): Pair<SwapProvider, SwapState> {
return provider to loadQuoteData(
return provider to loadCexQuoteData(
exchangeProviderType = ExchangeProviderType.CEX,
networkId = networkId,
amount = amount,
@ -340,7 +340,6 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
@Deprecated("used in old swap mechanism")
override suspend fun onSwap(
swapProvider: SwapProvider,
networkId: String,
@ -348,17 +347,22 @@ internal class SwapInteractorImpl @Inject constructor(
currencyToSend: CryptoCurrencyStatus,
currencyToGet: CryptoCurrencyStatus,
amountToSwap: String,
includeFeeInAmount: IncludeFeeInAmount,
fee: TxFee,
): TxState {
return when (swapProvider.type) {
ExchangeProviderType.CEX -> {
val amountDecimal = toBigDecimalOrNull(amountToSwap)
val amount = SwapAmount(requireNotNull(amountDecimal), getTokenDecimals(currencyToSend.currency))
val amountToSwapWithFee = if (includeFeeInAmount is IncludeFeeInAmount.Included) {
includeFeeInAmount.amountSubtractFee
} else {
amount
}
onSwapCex(
currencyToSend = currencyToSend,
currencyToGet = currencyToGet,
amount = amount,
amount = amountToSwapWithFee,
txFee = fee,
swapProvider = swapProvider,
userWalletId = requireNotNull(getSelectedWallet()).walletId,
@ -389,20 +393,19 @@ internal class SwapInteractorImpl @Inject constructor(
return state
}
val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency))
val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = state.txFee)
val isBalanceIncludeFeeEnough =
isBalanceEnough(fromToken, amount, feeByPriority)
val isFeeEnough = checkFeeIsEnough(
fee = feeByPriority,
spendAmount = amount,
val includeFeeInAmount = getIncludeFeeInAmount(
networkId = networkId,
txFee = state.txFee,
amount = amount,
fromToken = fromToken.currency,
selectedFee = selectedFee,
)
return state.copy(
permissionState = PermissionDataState.Empty,
preparedSwapConfigState = state.preparedSwapConfigState.copy(
isBalanceEnough = isBalanceIncludeFeeEnough,
isFeeEnough = isFeeEnough,
isBalanceEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough,
isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough,
includeFeeInAmount = includeFeeInAmount,
),
)
}
@ -666,7 +669,7 @@ internal class SwapInteractorImpl @Inject constructor(
* Load quote data calls only if spend is not allowed for token contract address
*/
@Suppress("LongParameterList")
private suspend fun loadQuoteData(
private suspend fun loadCexQuoteData(
exchangeProviderType: ExchangeProviderType,
networkId: String,
amount: SwapAmount,
@ -680,12 +683,31 @@ internal class SwapInteractorImpl @Inject constructor(
val fromToken = fromTokenStatus.currency
val toToken = toTokenStatus.currency
return coroutineScope {
val txFee = if (provider.type == ExchangeProviderType.CEX) {
getFeeForCex(amount, fromTokenStatus, networkId)
} else {
TxFeeState.Empty
}
val includeFeeInAmount = getIncludeFeeInAmount(
networkId = networkId,
txFee = txFee,
amount = amount,
fromToken = fromToken,
selectedFee = selectedFee,
)
val amountToRequest = if (includeFeeInAmount is IncludeFeeInAmount.Included) {
includeFeeInAmount.amountSubtractFee
} else {
amount
}
val quotes = repository.findBestQuote(
fromContractAddress = fromToken.getContractAddress(),
fromNetwork = fromToken.network.backendId,
toContractAddress = toToken.getContractAddress(),
toNetwork = toToken.network.backendId,
fromAmount = amount.toStringWithRightOffset(),
fromAmount = amountToRequest.toStringWithRightOffset(),
fromDecimals = amount.decimals,
providerId = provider.providerId,
toDecimals = toToken.decimals,
@ -701,8 +723,8 @@ internal class SwapInteractorImpl @Inject constructor(
networkId = networkId,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
providerType = provider.type,
selectedFee = selectedFee,
txFee = txFee,
includeFeeInAmount = includeFeeInAmount,
)
}
}
@ -716,16 +738,11 @@ internal class SwapInteractorImpl @Inject constructor(
networkId: String,
isAllowedToSpend: Boolean,
isBalanceWithoutFeeEnough: Boolean,
providerType: ExchangeProviderType,
selectedFee: FeeType,
txFee: TxFeeState,
includeFeeInAmount: IncludeFeeInAmount,
): SwapState {
val quoteModel = quoteDataModel.dataModel
if (quoteModel != null) {
val txFee = if (providerType == ExchangeProviderType.CEX) {
getFeeForCex(amount, fromToken, networkId)
} else {
TxFeeState.Empty
}
val swapState = updateBalances(
networkId = networkId,
fromTokenStatus = fromToken,
@ -754,19 +771,13 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
ExchangeProviderType.CEX -> {
val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFee)
val isFeeEnough = checkFeeIsEnough(
fee = feeByPriority,
spendAmount = amount,
networkId = networkId,
fromToken = fromToken.currency,
)
swapState.copy(
permissionState = PermissionDataState.Empty,
preparedSwapConfigState = PreparedSwapConfigState(
isFeeEnough = isFeeEnough,
isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough,
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceWithoutFeeEnough,
includeFeeInAmount = includeFeeInAmount,
),
)
}
@ -783,6 +794,45 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
private suspend fun getIncludeFeeInAmount(
networkId: String,
txFee: TxFeeState,
amount: SwapAmount,
fromToken: CryptoCurrency,
selectedFee: FeeType,
): IncludeFeeInAmount {
if (fromToken is CryptoCurrency.Token) {
return IncludeFeeInAmount.Excluded
}
val feeValue = when (txFee) {
TxFeeState.Empty -> BigDecimal.ZERO
is TxFeeState.MultipleFeeState -> if (selectedFee == FeeType.NORMAL) {
txFee.normalFee.feeValue
} else {
txFee.priorityFee.feeValue
}
is TxFeeState.SingleFeeState -> txFee.fee.feeValue
}
val tokenForFeeBalance =
userWalletManager.getNativeTokenBalance(networkId, derivationPath) ?: ProxyAmount.empty()
val amountWithFee = amount.value + feeValue
return if (amountWithFee < tokenForFeeBalance.value) {
IncludeFeeInAmount.Excluded
} else {
if (feeValue < amount.value) {
IncludeFeeInAmount.Included(
SwapAmount(
tokenForFeeBalance.value - feeValue,
transactionManager.getNativeTokenDecimals(networkId),
),
)
} else {
IncludeFeeInAmount.BalanceNotEnough
}
}
}
private suspend fun getFormattedFiatFees(networkId: String, vararg fees: BigDecimal): List<String> {
val appCurrency = userWalletManager.getUserAppCurrency()
val nativeToken = repository.getNativeTokenForNetwork(networkId)
@ -798,7 +848,7 @@ internal class SwapInteractorImpl @Inject constructor(
* Load swap data calls only if spend is allowed for token contract address
*/
@Suppress("LongParameterList")
private suspend fun loadSwapData(
private suspend fun loadDexSwapData(
provider: SwapProvider,
networkId: String,
fromToken: CryptoCurrencyStatus,
@ -856,6 +906,7 @@ internal class SwapInteractorImpl @Inject constructor(
isAllowedToSpend = true,
isBalanceEnough = isBalanceIncludeFeeEnough,
isFeeEnough = isFeeEnough,
includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex
),
)
} else {

View file

@ -1,5 +1,7 @@
package com.tangem.feature.swap.domain.models.domain
import com.tangem.feature.swap.domain.models.SwapAmount
/**
* Prepared swap config state that contains flags to determine
*
@ -7,8 +9,16 @@ package com.tangem.feature.swap.domain.models.domain
* @property isBalanceEnough shows is balance of token enough
* @property isFeeEnough shows is amount of main coin enough for fee
*/
// todo Refactor this state
data class PreparedSwapConfigState(
val isAllowedToSpend: Boolean,
val isBalanceEnough: Boolean,
val isFeeEnough: Boolean,
)
val includeFeeInAmount: IncludeFeeInAmount,
)
sealed class IncludeFeeInAmount {
data class Included(val amountSubtractFee: SwapAmount) : IncludeFeeInAmount()
object Excluded : IncludeFeeInAmount()
object BalanceNotEnough : IncludeFeeInAmount()
}

View file

@ -3,6 +3,7 @@ package com.tangem.feature.swap.domain.models.ui
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount
import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import java.math.BigDecimal
@ -18,6 +19,7 @@ sealed interface SwapState {
isAllowedToSpend = false,
isBalanceEnough = false,
isFeeEnough = false,
includeFeeInAmount = IncludeFeeInAmount.Excluded,
),
val permissionState: PermissionDataState = PermissionDataState.Empty,
val swapDataModel: SwapDataModel? = null,

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.CircularProgressIndicator
@ -110,17 +111,21 @@ private fun ProviderContentState(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing12),
) {
Row {
Text(
text = state.name,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = state.type,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
)
AnimatedContent(targetState = state.name, label = "") {
Text(
text = it,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
)
}
AnimatedContent(targetState = state.type, label = "") {
Text(
text = it,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
)
}
when (state.additionalBadge) {
ProviderState.AdditionalBadge.BestTrade ->
BestTradeItem(Modifier.padding(start = TangemTheme.dimens.spacing4))
@ -137,23 +142,27 @@ private fun ProviderContentState(
end = TangemTheme.dimens.spacing56,
),
) {
Text(
text = state.subtitle.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
if (state.percentLowerThenBest != null) {
AnimatedContent(targetState = state.subtitle, label = "") {
Text(
text = "-${state.percentLowerThenBest}%",
text = it.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.warning,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
color = TangemTheme.colors.text.tertiary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
if (state.percentLowerThenBest != null) {
AnimatedContent(targetState = state.percentLowerThenBest, label = "") {
Text(
text = "-$it%",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.warning,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
}
}
}
@ -198,24 +207,30 @@ private fun ProviderUnavailableState(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing12),
) {
Row {
AnimatedContent(targetState = state.name, label = "") {
Text(
text = it,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
AnimatedContent(targetState = state.type, label = "") {
Text(
text = it,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
)
}
}
AnimatedContent(targetState = state.alertText, label = "") {
Text(
text = state.name,
style = TangemTheme.typography.caption2,
text = it.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
Text(
text = state.type,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
Text(
text = state.alertText.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
}

View file

@ -14,9 +14,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.converters.TokensDataConverter
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.models.*
@ -211,41 +209,7 @@ internal class StateBuilder(
): SwapStateHolder {
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
val warnings = mutableListOf<SwapWarning>()
if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
quoteModel.preparedSwapConfigState.isFeeEnough &&
quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest
) {
warnings.add(
SwapWarning.PermissionNeeded(
createPermissionNotificationConfig(fromToken.symbol),
),
)
}
if (!quoteModel.preparedSwapConfigState.isFeeEnough &&
quoteModel.preparedSwapConfigState.isBalanceEnough
) {
warnings.add(
SwapWarning.UnableToCoverFeeWarning(
createUnableToCoverFeeNotificationConfig(
fromToken = fromToken,
onBuyClick = actions.onBuyClick,
),
),
)
}
if (!quoteModel.preparedSwapConfigState.isBalanceEnough) {
warnings.add(SwapWarning.InsufficientFunds)
}
if (quoteModel.priceImpact > PRICE_IMPACT_THRESHOLD) {
warnings.add(
SwapWarning.HighPriceImpact(
priceImpact = (quoteModel.priceImpact * HUNDRED_PERCENTS).toInt(),
notificationConfig = highPriceImpactNotificationConfig(),
),
)
}
val warnings = getWarningsForSuccessState(quoteModel, fromToken)
val feeState = createFeeState(quoteModel.txFee, selectedFeeType)
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
@ -288,9 +252,7 @@ internal class StateBuilder(
),
fee = feeState,
swapButton = SwapButton(
enabled = quoteModel.preparedSwapConfigState.isAllowedToSpend &&
quoteModel.preparedSwapConfigState.isBalanceEnough &&
quoteModel.preparedSwapConfigState.isFeeEnough,
enabled = getSwapButtonEnabled(quoteModel.preparedSwapConfigState),
loading = false,
onClick = actions.onSwapClick,
),
@ -306,6 +268,71 @@ internal class StateBuilder(
)
}
private fun getWarningsForSuccessState(
quoteModel: SwapState.QuotesLoadedState,
fromToken: CryptoCurrency,
): List<SwapWarning> {
val warnings = mutableListOf<SwapWarning>()
if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
quoteModel.preparedSwapConfigState.isFeeEnough &&
quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest
) {
warnings.add(
SwapWarning.PermissionNeeded(
createPermissionNotificationConfig(fromToken.symbol),
),
)
}
when (quoteModel.preparedSwapConfigState.includeFeeInAmount) {
is IncludeFeeInAmount.Included ->
warnings.add(
SwapWarning.GeneralWarning(
createNetworkFeeCoverageNotificationConfig(),
),
)
else -> Unit
}
if (!quoteModel.preparedSwapConfigState.isFeeEnough &&
quoteModel.preparedSwapConfigState.isBalanceEnough
) {
warnings.add(
SwapWarning.UnableToCoverFeeWarning(
createUnableToCoverFeeNotificationConfig(
fromToken = fromToken,
onBuyClick = actions.onBuyClick,
),
),
)
}
// check isBalanceEnough, but for dex includeFeeInAmount always Excluded
if (!quoteModel.preparedSwapConfigState.isBalanceEnough &&
quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included
) {
warnings.add(SwapWarning.InsufficientFunds)
}
if (quoteModel.priceImpact > PRICE_IMPACT_THRESHOLD) {
warnings.add(
SwapWarning.HighPriceImpact(
priceImpact = (quoteModel.priceImpact * HUNDRED_PERCENTS).toInt(),
notificationConfig = highPriceImpactNotificationConfig(),
),
)
}
return warnings
}
private fun getSwapButtonEnabled(preparedSwapConfigState: PreparedSwapConfigState): Boolean {
return when (preparedSwapConfigState.includeFeeInAmount) {
IncludeFeeInAmount.BalanceNotEnough -> false
IncludeFeeInAmount.Excluded ->
preparedSwapConfigState.isAllowedToSpend &&
preparedSwapConfigState.isBalanceEnough &&
preparedSwapConfigState.isFeeEnough
is IncludeFeeInAmount.Included -> true
}
}
fun createQuotesErrorState(
uiStateHolder: SwapStateHolder,
swapProvider: SwapProvider,
@ -406,7 +433,7 @@ internal class StateBuilder(
notificationConfig = NotificationConfig(
title = resourceReference(R.string.common_error),
subtitle = resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString())),
iconResId = R.drawable.ic_alert_circle_24,
iconResId = R.drawable.img_attention_20,
),
)
}
@ -900,6 +927,14 @@ internal class StateBuilder(
),
)
}
private fun createNetworkFeeCoverageNotificationConfig(): NotificationConfig {
return NotificationConfig(
title = resourceReference(R.string.send_network_fee_warning_title),
subtitle = resourceReference(R.string.send_network_fee_warning_content),
iconResId = R.drawable.img_attention_20,
)
}
// end region
private fun getShortAddressValue(fullAddress: String): String {

View file

@ -74,7 +74,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi
if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings)
if (state.permissionState is SwapPermissionState.InProgress) {
AnimatedVisibility(visible = state.permissionState is SwapPermissionState.InProgress) {
CardWithIcon(
title = stringResource(id = R.string.swapping_pending_transaction_title),
description = stringResource(id = R.string.swapping_pending_transaction_subtitle),
@ -236,6 +236,7 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) {
}
}
@Suppress("LongMethod")
@Composable
private fun SwapWarnings(warnings: List<SwapWarning>) {
Column(
@ -289,7 +290,6 @@ private fun SwapWarnings(warnings: List<SwapWarning>) {
is SwapWarning.GeneralWarning -> {
Notification(
config = warning.notificationConfig,
iconTint = TangemTheme.colors.icon.warning,
)
}
else -> {}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap.ui
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -194,14 +195,16 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie
)
SpacerW16()
if (balance.isNotBlank()) {
Text(
text = balance,
color = TangemTheme.colors.text.tertiary,
style = MaterialTheme.typography.body2,
modifier = Modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size20)
.padding(top = TangemTheme.dimens.spacing2),
)
AnimatedContent(targetState = balance, label = "") {
Text(
text = it,
color = TangemTheme.colors.text.tertiary,
style = MaterialTheme.typography.body2,
modifier = Modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size20)
.padding(top = TangemTheme.dimens.spacing2),
)
}
} else {
RectangleShimmer(
modifier = Modifier
@ -259,12 +262,14 @@ private fun Content(
}
}
is TransactionCardType.SendCard -> {
AutoSizeTextField(
modifier = sumTextModifier,
textFieldValue = textFieldValue ?: TextFieldValue(),
onAmountChange = { type.onAmountChanged(it) },
onFocusChange = type.onFocusChanged,
)
AnimatedContent(targetState = textFieldValue, label = "") {
AutoSizeTextField(
modifier = sumTextModifier,
textFieldValue = it ?: TextFieldValue(),
onAmountChange = { type.onAmountChanged(it) },
onFocusChange = type.onFocusChanged,
)
}
}
}
@ -292,12 +297,14 @@ private fun Content(
)
}
} else {
Text(
text = amountEquivalent,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20),
)
AnimatedContent(targetState = amountEquivalent, label = "") {
Text(
text = it,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20),
)
}
}
} else {
RectangleShimmer(

View file

@ -381,15 +381,22 @@ internal class SwapViewModel @Inject constructor(
private fun onSwapClick() {
singleTaskScheduler.cancelTask()
uiState = stateBuilder.createSwapInProgressState(uiState)
val provider = requireNotNull(dataState.selectedProvider) { "Selected provider is null" }
val lastLoadedQuotesState = dataState.lastLoadedSwapStates[provider] as? SwapState.QuotesLoadedState
if (lastLoadedQuotesState == null) {
Timber.e("Last loaded quotes state is null")
return
}
viewModelScope.launch(dispatchers.main) {
runCatching(dispatchers.io) {
swapInteractor.onSwap(
swapProvider = requireNotNull(dataState.selectedProvider),
swapProvider = provider,
networkId = dataState.networkId,
swapData = dataState.swapDataModel,
currencyToSend = requireNotNull(dataState.fromCryptoCurrency),
currencyToGet = requireNotNull(dataState.toCryptoCurrency),
amountToSwap = requireNotNull(dataState.amount),
includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount,
fee = requireNotNull(dataState.selectedFee),
)
}

View file

@ -13,4 +13,11 @@ data class ProxyAmount(
val currencySymbol: String,
var value: BigDecimal,
val decimals: Int,
)
) {
companion object {
fun empty(): ProxyAmount {
return ProxyAmount("", BigDecimal.ZERO, 0)
}
}
}