diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt index d0d3315f20..fe2a681d08 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt @@ -102,7 +102,7 @@ fun ClickableWarningCard( * >Figma component */ @Composable -fun RefreshableWaringCard( +fun RefreshableWarningCard( title: String, description: String, icon: @Composable (() -> Unit)? = null, @@ -193,7 +193,7 @@ private fun WarningsPreview() { onClick = {}, ) SpacerH32() - RefreshableWaringCard( + RefreshableWarningCard( title = "Exchange rate has expired", description = "To access all the networks, you need to scan the card.", onClick = {}, diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt index d7f7790479..318b5635de 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt @@ -7,4 +7,6 @@ sealed class Warning { data class ExistentialDepositWarning(val existentialDeposit: BigDecimal) : Warning() data class MinAmountWarning(val dustValue: BigDecimal) : Warning() + + data class ReduceAmountWarning(val tezosFeeThreshold: BigDecimal) : Warning() } \ No newline at end of file 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 d3ec1058b9..b43322b46a 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 @@ -79,8 +79,6 @@ interface SwapInteractor { */ fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount - fun isAvailableToSwap(networkId: String): Boolean - fun getSelectedWallet(): UserWallet? suspend fun selectInitialCurrencyToSwap( 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 c1a72132a4..74d9cdd518 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 @@ -197,7 +197,6 @@ internal class SwapInteractorImpl @Inject constructor( return repository.getPairs(initialCurrency, currenciesList) } - @Deprecated("used in old swap mechanism") override suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState { val derivationPath = permissionOptions.fromToken.network.derivationPath.value val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) { @@ -241,7 +240,6 @@ internal class SwapInteractorImpl @Inject constructor( } } - @Deprecated("used in old swap mechanism") override suspend fun findBestQuote( fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, @@ -376,6 +374,7 @@ internal class SwapInteractorImpl @Inject constructor( val warnings = mutableListOf() manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken) manageDustWarning(warnings, feeState, userWalletId, fromTokenStatus, amount) + manageReduceAmountWarning(warnings, fromTokenStatus, amount) return warnings } @@ -424,6 +423,17 @@ internal class SwapInteractorImpl @Inject constructor( } } + private fun manageReduceAmountWarning( + warnings: MutableList, + fromTokenStatus: CryptoCurrencyStatus, + amount: SwapAmount, + ) { + val isTezos = fromTokenStatus.currency.network.id.value == Blockchain.Tezos.id + if (isTezos && amount.value == fromTokenStatus.value.amount) { + warnings.add(Warning.ReduceAmountWarning(TEZOS_FEE_THRESHOLD)) + } + } + override suspend fun onSwap( swapProvider: SwapProvider, swapData: SwapDataModel?, @@ -729,16 +739,10 @@ internal class SwapInteractorImpl @Inject constructor( ) } - @Deprecated("used in old swap mechanism") override fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount { return SwapAmount(token.value.amount ?: BigDecimal.ZERO, token.currency.decimals) } - @Deprecated("used in old swap mechanism") - override fun isAvailableToSwap(networkId: String): Boolean { - return ONE_INCH_SUPPORTED_NETWORKS.contains(networkId) - } - override suspend fun selectInitialCurrencyToSwap( initialCryptoCurrency: CryptoCurrency, state: TokensDataStateExpress, @@ -1575,16 +1579,6 @@ internal class SwapInteractorImpl @Inject constructor( private const val INCREASE_GAS_LIMIT_BY = 112 // 12% private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% private const val INFINITY_SYMBOL = "∞" - - private val ONE_INCH_SUPPORTED_NETWORKS = listOf( - "ethereum", - "binance-smart-chain", - "polygon-pos", - "optimistic-ethereum", - "arbitrum-one", - "xdai", - "avalanche", - "fantom", - ) + private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01") } } \ No newline at end of file diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 8986bf7e0d..c78375b5e4 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -67,4 +67,5 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index cff24ca2a6..e5db542bba 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -20,6 +20,7 @@ data class SwapStateHolder( val alert: SwapWarning.GenericWarning? = null, val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED, val providerState: ProviderState, + val reduceAmountIgnore: Boolean, // ignore warning about reducing XTZ amount by 0.01 val fee: FeeItemState = FeeItemState.Empty, val permissionState: SwapPermissionState = SwapPermissionState.Empty, @@ -115,6 +116,8 @@ sealed interface SwapWarning { data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning data class GeneralInformational(val notificationConfig: NotificationConfig) : SwapWarning data class TransactionInProgressWarning(val title: TextReference, val description: TextReference) : SwapWarning + data class NeedReserveToCreateAccount(val notificationConfig: NotificationConfig) : SwapWarning + data class ReduceAmount(val notificationConfig: NotificationConfig) : SwapWarning } enum class GenericWarningType { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index e7641eca69..bd84a05cac 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.models import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.ui.TxFee data class UiActions( @@ -13,6 +14,8 @@ data class UiActions( val onChangeCardsClicked: () -> Unit, val onBackClicked: () -> Unit, val onMaxAmountSelected: () -> Unit, + val onReduceAmount: (SwapAmount) -> Unit, + val onReduceAmountIgnoreClick: () -> Unit, val openPermissionBottomSheet: () -> Unit, val onChangeApproveType: (ApproveType) -> Unit, // region new actions 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 1580582c2f..2587562721 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 @@ -88,6 +88,7 @@ internal class StateBuilder( onShowPermissionBottomSheet = actions.openPermissionBottomSheet, providerState = ProviderState.Empty(), shouldShowMaxAmount = false, + reduceAmountIgnore = false, priceImpact = PriceImpact.Empty(), ) } @@ -213,7 +214,11 @@ internal class StateBuilder( ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder - val warnings = getWarningsForSuccessState(quoteModel, fromToken) + val warnings = getWarningsForSuccessState( + quoteModel = quoteModel, + fromToken = fromToken, + ignoreAmountReduce = uiStateHolder.reduceAmountIgnore, + ) val feeState = createFeeState(quoteModel.txFee, selectedFeeType) val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus @@ -311,36 +316,23 @@ internal class StateBuilder( private fun getWarningsForSuccessState( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, + ignoreAmountReduce: Boolean, ): List { val warnings = mutableListOf() - addDomainWarnings(quoteModel, warnings) - if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && - quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough && - 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 - } - addUnableCoverFeeWarning(quoteModel, fromToken, warnings) - // check isBalanceEnough, but for dex includeFeeInAmount always Excluded - if (!quoteModel.preparedSwapConfigState.isBalanceEnough && - quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included - ) { - warnings.add(SwapWarning.InsufficientFunds) - } + maybeAddDomainWarnings(quoteModel, warnings, ignoreAmountReduce) + maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings) + maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken) + maybeAddNetworkFeeCoverageWarning(quoteModel, warnings) + maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings) + maybeAddInsufficientFundsWarning(quoteModel, warnings) + maybeAddTransactionInProgressWarning(quoteModel, warnings) + return warnings + } + private fun maybeAddTransactionInProgressWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ) { if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { warnings.add( SwapWarning.TransactionInProgressWarning( @@ -361,10 +353,13 @@ internal class StateBuilder( ), ) } - return warnings } - private fun addDomainWarnings(quoteModel: SwapState.QuotesLoadedState, warnings: MutableList) { + private fun maybeAddDomainWarnings( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ignoreAmountReduce: Boolean, + ) { quoteModel.warnings.forEach { when (it) { is Warning.ExistentialDepositWarning -> { @@ -401,11 +396,84 @@ internal class StateBuilder( ), ) } + is Warning.ReduceAmountWarning -> { + if (!ignoreAmountReduce) { + warnings.add( + SwapWarning.ReduceAmount( + notificationConfig = createReduceAmountNotificationConfig( + amount = it.tezosFeeThreshold.toPlainString(), + onConfirmClick = { + val fromAmount = quoteModel.fromTokenInfo.tokenAmount + val patchedAmount = fromAmount.copy( + value = fromAmount.value - it.tezosFeeThreshold, + ) + actions.onReduceAmount(patchedAmount) + }, + onDismissClick = actions.onReduceAmountIgnoreClick, + ), + ), + ) + } + } } } } - private fun addUnableCoverFeeWarning( + private fun maybeAddNeedReserveToCreateAccountWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ) { + val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value + if (status is CryptoCurrencyStatus.NoAccount) { + val amount = quoteModel.toTokenInfo.tokenAmount.value + val amountToCreateAccount = status.amountToCreateAccount + + if (amount < amountToCreateAccount) { + warnings.add( + SwapWarning.NeedReserveToCreateAccount( + notificationConfig = createActivateAccountNotificationConfig( + status.amountToCreateAccount, + quoteModel.toTokenInfo.cryptoCurrencyStatus.currency.name, + ), + ), + ) + } + } + } + + private fun maybeAddPermissionNeededWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + fromToken: CryptoCurrency, + ) { + if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && + quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough && + quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest + ) { + warnings.add( + SwapWarning.PermissionNeeded( + createPermissionNotificationConfig(fromToken.symbol), + ), + ) + } + } + + private fun maybeAddNetworkFeeCoverageWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ) { + when (quoteModel.preparedSwapConfigState.includeFeeInAmount) { + is IncludeFeeInAmount.Included -> + warnings.add( + SwapWarning.GeneralWarning( + createNetworkFeeCoverageNotificationConfig(), + ), + ) + else -> Unit + } + } + + private fun maybeAddUnableCoverFeeWarning( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, warnings: MutableList, @@ -428,7 +496,29 @@ internal class StateBuilder( } } + private fun maybeAddInsufficientFundsWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ) { + // check isBalanceEnough, but for dex includeFeeInAmount always Excluded + if (!quoteModel.preparedSwapConfigState.isBalanceEnough && + quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included + ) { + warnings.add(SwapWarning.InsufficientFunds) + } + } + private fun getSwapButtonEnabled(quoteModel: SwapState.QuotesLoadedState): Boolean { + val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value + if (status is CryptoCurrencyStatus.NoAccount) { + val amount = quoteModel.toTokenInfo.tokenAmount.value + val amountToCreateAccount = status.amountToCreateAccount + + if (amount < amountToCreateAccount) { + return false + } + } + val preparedSwapConfigState = quoteModel.preparedSwapConfigState // check has has outgoing transaction if (preparedSwapConfigState.hasOutgoingTransaction) return false @@ -1212,6 +1302,35 @@ internal class StateBuilder( ) } + private fun createActivateAccountNotificationConfig(amount: BigDecimal, token: String): NotificationConfig { + return NotificationConfig( + title = resourceReference( + id = R.string.send_notification_invalid_reserve_amount_title, + formatArgs = wrappedList("$amount $token"), + ), + subtitle = resourceReference(R.string.send_notification_invalid_reserve_amount_text), + iconResId = R.drawable.img_attention_20, + ) + } + + private fun createReduceAmountNotificationConfig( + amount: String, + onConfirmClick: () -> Unit, + onDismissClick: () -> Unit, + ): NotificationConfig { + return NotificationConfig( + title = resourceReference(R.string.send_notification_high_fee_title), + subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)), + iconResId = R.drawable.img_attention_20, + buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig( + primaryText = resourceReference(R.string.xtz_withdrawal_message_reduce, wrappedList(amount)), + onPrimaryClick = onConfirmClick, + secondaryText = resourceReference(R.string.xtz_withdrawal_message_ignore), + onSecondaryClick = onDismissClick, + ), + ) + } + private fun createUnableToCoverFeeNotificationConfig( fromToken: CryptoCurrency, feeCurrency: CryptoCurrency?, 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 64964aef80..fa48e3f6fb 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 @@ -339,7 +339,7 @@ private fun SwapWarnings(warnings: List) { is SwapWarning.GenericWarning -> { val message = warning.message?.resolveReference() ?: stringResource(id = R.string.common_unknown_error) - RefreshableWaringCard( + RefreshableWarningCard( title = stringResource(id = R.string.common_warning), description = message, onClick = warning.onClick, @@ -372,6 +372,16 @@ private fun SwapWarnings(warnings: List) { iconTint = TangemTheme.colors.icon.accent, ) } + is SwapWarning.NeedReserveToCreateAccount -> { + Notification( + config = warning.notificationConfig, + ) + } + is SwapWarning.ReduceAmount -> { + Notification( + config = warning.notificationConfig, + ) + } is SwapWarning.TransactionInProgressWarning -> { CardWithIcon( title = warning.title.resolveReference(), @@ -494,6 +504,7 @@ private val state = SwapStateHolder( providerState = ProviderState.Loading(), priceImpact = PriceImpact.Empty(), shouldShowMaxAmount = true, + reduceAmountIgnore = false, tosState = TosState( tosLink = LegalState( title = stringReference("Terms of Use"), 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 a559fb5054..2077c47764 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 @@ -27,6 +27,7 @@ import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.ExpressException +import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* @@ -810,6 +811,10 @@ internal class SwapViewModel @Inject constructor( } } + private fun onReduceAmountClicked(newAmount: SwapAmount) { + onAmountChanged(newAmount.formatToUIRepresentation()) + } + @Suppress("UnusedPrivateMember") private fun onAmountSelected(selected: Boolean) { if (selected) { @@ -872,7 +877,14 @@ internal class SwapViewModel @Inject constructor( } onSearchEntered("") }, - onMaxAmountSelected = { onMaxAmountClicked() }, + onMaxAmountSelected = ::onMaxAmountClicked, + onReduceAmount = ::onReduceAmountClicked, + onReduceAmountIgnoreClick = { + uiState = uiState.copy( + reduceAmountIgnore = true, + warnings = uiState.warnings.filter { it !is SwapWarning.ReduceAmount }, + ) + }, openPermissionBottomSheet = { singleTaskScheduler.cancelTask() analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) @@ -1151,11 +1163,11 @@ internal class SwapViewModel @Inject constructor( ) } - companion object { - private const val loggingTag = "SwapViewModel" - private const val INITIAL_AMOUNT = "" - private const val UPDATE_DELAY = 10000L - private const val DEBOUNCE_AMOUNT_DELAY = 1000L - private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L + private companion object { + const val loggingTag = "SwapViewModel" + const val INITIAL_AMOUNT = "" + const val UPDATE_DELAY = 10000L + const val DEBOUNCE_AMOUNT_DELAY = 1000L + const val UPDATE_BALANCE_DELAY_MILLIS = 11000L } } \ No newline at end of file