diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt index 86817c690a..0c2a785acc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt @@ -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) { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 01531715e0..06e956fa49 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -99,7 +99,7 @@ internal class DefaultCurrenciesRepository( ) storeAndPushTokens( userWalletId = userWalletId, - response = updatedResponse + response = updatedResponse, ) fetchExchangeableUserMarketCoinsByIds(userWalletId, updatedResponse) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 9c8ebfcbf3..d36610f673 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -64,6 +64,7 @@ interface SwapInteractor { currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, amountToSwap: String, + includeFeeInAmount: IncludeFeeInAmount, fee: TxFee, ): TxState diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 5d93881660..0e3c786b0a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -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 { - 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 { 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 { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt index 8b9f1c963b..53cfae4ded 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt @@ -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, -) \ No newline at end of file + val includeFeeInAmount: IncludeFeeInAmount, +) + +sealed class IncludeFeeInAmount { + data class Included(val amountSubtractFee: SwapAmount) : IncludeFeeInAmount() + object Excluded : IncludeFeeInAmount() + object BalanceNotEnough : IncludeFeeInAmount() +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 93a5795340..d031891ba3 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -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, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 79bfe25d5e..3dead8dfcd 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -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), - ) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 44ea71aac2..6d540ed412 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -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() - 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 { + val warnings = mutableListOf() + 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 { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 0ba2059fab..949ead0f64 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -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) { Column( @@ -289,7 +290,6 @@ private fun SwapWarnings(warnings: List) { is SwapWarning.GeneralWarning -> { Notification( config = warning.notificationConfig, - iconTint = TangemTheme.colors.icon.warning, ) } else -> {} diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index f4bd0ae799..258e8f5ddf 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -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( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 65d865ae21..64afcb59a5 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -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), ) } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyAmount.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyAmount.kt index aa4c58b0f3..4a877ccca3 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyAmount.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyAmount.kt @@ -13,4 +13,11 @@ data class ProxyAmount( val currencySymbol: String, var value: BigDecimal, val decimals: Int, -) \ No newline at end of file +) { + + companion object { + fun empty(): ProxyAmount { + return ProxyAmount("", BigDecimal.ZERO, 0) + } + } +} \ No newline at end of file