Updated on 2026-08-14
This commit is contained in:
parent
54f13f1082
commit
d5d03d73f2
12 changed files with 289 additions and 149 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ internal class DefaultCurrenciesRepository(
|
|||
)
|
||||
storeAndPushTokens(
|
||||
userWalletId = userWalletId,
|
||||
response = updatedResponse
|
||||
response = updatedResponse,
|
||||
)
|
||||
fetchExchangeableUserMarketCoinsByIds(userWalletId, updatedResponse)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ interface SwapInteractor {
|
|||
currencyToSend: CryptoCurrencyStatus,
|
||||
currencyToGet: CryptoCurrencyStatus,
|
||||
amountToSwap: String,
|
||||
includeFeeInAmount: IncludeFeeInAmount,
|
||||
fee: TxFee,
|
||||
): TxState
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 -> {}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue