From cd61d9c841cc2f19e152cfc138532291dd6c3cad Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jan 2024 16:53:00 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 18 ++ ...GetBalanceNotEnoughForFeeWarningUseCase.kt | 97 ++++++++++ .../IsAmountSubtractAvailableUseCase.kt | 29 +++ .../impl/presentation/state/SendUiState.kt | 1 + .../state/fee/FeeNotificationFactory.kt | 112 ++++++++---- .../presentation/state/fee/FeeStateFactory.kt | 166 +++++++++--------- .../state/fee/SendFeeStateConverter.kt | 1 + .../ui/fee/SendSpeedAndFeeContent.kt | 31 +++- .../presentation/viewmodel/SendViewModel.kt | 57 ++++-- 9 files changed, 369 insertions(+), 143 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 336c1129ed..b439d53019 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -310,4 +310,22 @@ internal object TokensDomainModule { ): GetNetworksSupportedByWallet { return GetNetworksSupportedByWallet(repository = repository) } + + @Provides + @ViewModelScoped + fun provideGetBalanceNotEnoughForFeeWarningUseCase( + currenciesRepository: CurrenciesRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetBalanceNotEnoughForFeeWarningUseCase { + return GetBalanceNotEnoughForFeeWarningUseCase(currenciesRepository, dispatchers) + } + + @Provides + @ViewModelScoped + fun provideIsAmountSubtractAvailableUseCase( + currenciesRepository: CurrenciesRepository, + dispatchers: CoroutineDispatcherProvider, + ): IsAmountSubtractAvailableUseCase { + return IsAmountSubtractAvailableUseCase(currenciesRepository, dispatchers) + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt new file mode 100644 index 0000000000..b1f054e498 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt @@ -0,0 +1,97 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.FeePaidCurrency +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +/** + * Use case for getting balance not enough warning to cover fee. + * + * This warning is shown when current currency is not paying fee and paying fee currency balance is not enough + * + * Current | Paying fee | Warning + * Coin | Coin | - + * Token | Coin | + + * Coin | PToken | + (VTO - VTHO) + * Token | PToken | + (Other VeChainToken - VTHO) + * PToken | PToken | - (VTHO - VTHO or TerraToken - TerraToken) + */ +class GetBalanceNotEnoughForFeeWarningUseCase( + private val currenciesRepository: CurrenciesRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + suspend operator fun invoke( + fee: BigDecimal, + userWalletId: UserWalletId, + tokenStatus: CryptoCurrencyStatus, + coinStatus: CryptoCurrencyStatus, + ): Either = Either.catch { + withContext(dispatchers.io) { + val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, tokenStatus.currency) + val coinBalance = coinStatus.value.amount ?: BigDecimal.ZERO + + val isFeePaidByCoin = tokenStatus.currency is CryptoCurrency.Token + val isFeePaidByToken = + feePaidCurrency is FeePaidCurrency.Token && tokenStatus.currency.id != feePaidCurrency.tokenId + + val warning = when { + feePaidCurrency is FeePaidCurrency.Coin && isFeePaidByCoin && fee > coinBalance -> { + CryptoCurrencyWarning.BalanceNotEnoughForFee( + tokenCurrency = tokenStatus.currency, + coinCurrency = coinStatus.currency, + ) + } + feePaidCurrency is FeePaidCurrency.Token && isFeePaidByToken && fee > feePaidCurrency.balance -> { + constructTokenBalanceNotEnoughWarning( + userWalletId = userWalletId, + tokenStatus = tokenStatus, + feePaidToken = feePaidCurrency, + ) + } + else -> null + } + warning + } + } + + /** + * Check if fee paying token [feePaidToken] is added to wallet [userWalletId] + */ + private suspend fun constructTokenBalanceNotEnoughWarning( + userWalletId: UserWalletId, + tokenStatus: CryptoCurrencyStatus, + feePaidToken: FeePaidCurrency.Token, + ): CryptoCurrencyWarning { + val token = currenciesRepository + .getMultiCurrencyWalletCurrenciesSync(userWalletId) + .find { + it is CryptoCurrency.Token && + it.contractAddress.equals(feePaidToken.contractAddress, ignoreCase = true) && + it.network.derivationPath == tokenStatus.currency.network.derivationPath + } + return if (token != null) { + CryptoCurrencyWarning.CustomTokenNotEnoughForFee( + currency = tokenStatus.currency, + feeCurrency = token, + networkName = token.network.name, + feeCurrencyName = feePaidToken.name, + feeCurrencySymbol = feePaidToken.symbol, + ) + } else { + CryptoCurrencyWarning.CustomTokenNotEnoughForFee( + currency = tokenStatus.currency, + feeCurrency = null, + networkName = tokenStatus.currency.network.name, + feeCurrencyName = feePaidToken.name, + feeCurrencySymbol = feePaidToken.symbol, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt new file mode 100644 index 0000000000..d003a95067 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.FeePaidCurrency +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +/** + * Use case for checking if currency amount can be subtracted. + * Amount can be subtracted if only it is paying fee + */ +class IsAmountSubtractAvailableUseCase( + private val currenciesRepository: CurrenciesRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either = + Either.catch { + withContext(dispatchers.io) { + when (val feeCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, currency)) { + is FeePaidCurrency.Coin -> currency is CryptoCurrency.Coin + is FeePaidCurrency.SameCurrency -> true + is FeePaidCurrency.Token -> currency.id == feeCurrency.tokenId + } + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index ea8245e94c..4407d17d0f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -68,6 +68,7 @@ internal sealed class SendStates { override val type: SendUiStateType = SendUiStateType.Fee, override val isPrimaryButtonEnabled: Boolean = false, val feeSelectorState: FeeSelectorState, + val isSubtractAvailable: Boolean, val isSubtract: Boolean, val isUserSubtracted: Boolean, val fee: Fee?, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt index 8f51e6164a..c434a6182f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt @@ -3,39 +3,53 @@ package com.tangem.features.send.impl.presentation.state.fee import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider -import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import java.math.BigDecimal internal class FeeNotificationFactory( private val coinCryptoCurrencyStatusProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, + private val currentStateProvider: Provider, private val userWalletProvider: Provider, private val clickIntents: SendClickIntents, + private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, ) { - operator fun invoke(feeState: SendStates.FeeState, amountValue: BigDecimal?): ImmutableList = - buildList { - when (val feeSelectorState = feeState.feeSelectorState) { - FeeSelectorState.Loading -> Unit - FeeSelectorState.Error -> { - addFeeUnreachableNotification(feeSelectorState) + fun create() = currentStateProvider().currentState + .filter { it == SendUiStateType.Fee } + .map { + val state = currentStateProvider() + val feeState = state.feeState ?: return@map persistentListOf() + buildList { + when (val feeSelectorState = feeState.feeSelectorState) { + FeeSelectorState.Loading -> Unit + FeeSelectorState.Error -> { + addFeeUnreachableNotification(feeSelectorState) + } + is FeeSelectorState.Content -> { + val customFee = feeSelectorState.customValues + val selectedFee = feeSelectorState.selectedFee + addTooLowNotification(feeSelectorState.fees, selectedFee, customFee) + addTooHighNotification(feeSelectorState.fees, selectedFee, customFee) + addFeeCoverageNotification(feeState, state.amountState) + addExceedsBalanceNotification(feeState.fee) + } } - is FeeSelectorState.Content -> { - val customFee = feeSelectorState.customValues - val selectedFee = feeSelectorState.selectedFee - addTooLowNotification(feeSelectorState.fees, selectedFee, customFee) - addTooHighNotification(feeSelectorState.fees, selectedFee, customFee) - addFeeCoverageNotification(feeState, amountValue) - addExceedsBalanceNotification(feeState.fee) - } - } - }.toImmutableList() + }.toImmutableList() + } private fun MutableList.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) { if (feeSelectorState is FeeSelectorState.Error) { @@ -72,32 +86,60 @@ internal class FeeNotificationFactory( private fun MutableList.addFeeCoverageNotification( feeState: SendStates.FeeState, - amountValue: BigDecimal?, + amountState: SendStates.AmountState?, ) { - val cryptoAmount = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO - val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO - val value = amountValue ?: BigDecimal.ZERO + if (!feeState.isSubtractAvailable) return + + val cryptoAmount = coinCryptoCurrencyStatusProvider().value.amount ?: return + val feeValue = feeState.fee?.amount?.value ?: return + val value = amountState?.amountTextField?.cryptoAmount?.value ?: return if (cryptoAmount <= value + feeValue && feeState.isSubtract && !feeState.isUserSubtracted) { add(SendFeeNotification.Warning.NetworkCoverage) } } - private fun MutableList.addExceedsBalanceNotification(fee: Fee?) { - val coinCryptoCurrency = coinCryptoCurrencyStatusProvider() - val cryptoAmount = coinCryptoCurrency.value.amount ?: BigDecimal.ZERO + private suspend fun MutableList.addExceedsBalanceNotification(fee: Fee?) { val feeValue = fee?.amount?.value ?: BigDecimal.ZERO + val userWalletId = userWalletProvider().walletId + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - if (feeValue > cryptoAmount) { - add( - SendFeeNotification.Error.ExceedsBalance( - coinCryptoCurrency.currency.networkIconResId, - ) { - clickIntents.onTokenDetailsClick( - userWalletProvider().walletId, - coinCryptoCurrency.currency, - ) - }, - ) + val warning = getBalanceNotEnoughForFeeWarningUseCase( + fee = feeValue, + userWalletId = userWalletId, + tokenStatus = cryptoCurrencyStatus, + coinStatus = coinCryptoCurrencyStatusProvider(), + ).fold( + ifLeft = { null }, + ifRight = { it }, + ) ?: return + + when (warning) { + is CryptoCurrencyWarning.BalanceNotEnoughForFee -> { + add( + SendFeeNotification.Error.ExceedsBalance( + warning.coinCurrency.networkIconResId, + ) { + clickIntents.onTokenDetailsClick( + userWalletProvider().walletId, + warning.coinCurrency, + ) + }, + ) + } + is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> { + val currency = warning.feeCurrency ?: warning.currency + add( + SendFeeNotification.Error.ExceedsBalance( + currency.networkIconResId, + ) { + clickIntents.onTokenDetailsClick( + userWalletId, + currency, + ) + }, + ) + } + else -> Unit } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index 8a1073cad3..dd003ef56f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -5,12 +5,12 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import com.tangem.utils.isNullOrZero +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal @@ -24,7 +24,6 @@ internal class FeeStateFactory( private val coinCryptoCurrencyStatusProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, private val appCurrencyProvider: Provider, - userWalletProvider: Provider, ) { private val customFeeFieldConverter by lazy { SendFeeCustomFieldConverter( @@ -33,12 +32,6 @@ internal class FeeStateFactory( ) } - private val feeNotificationFactory = FeeNotificationFactory( - coinCryptoCurrencyStatusProvider = coinCryptoCurrencyStatusProvider, - userWalletProvider = userWalletProvider, - clickIntents = clickIntents, - ) - fun onFeeOnLoadingState(): SendUiState { val state = currentStateProvider() val feeState = state.feeState ?: return state @@ -46,12 +39,12 @@ internal class FeeStateFactory( feeState = feeState.copy( feeSelectorState = FeeSelectorState.Loading, notifications = persistentListOf(), - isPrimaryButtonEnabled = feeState.isPrimaryButtonEnabled(), + isPrimaryButtonEnabled = false, ), ) } - fun onFeeOnLoadedState(fees: TransactionFee): SendUiState { + fun onFeeOnLoadedState(fees: TransactionFee, isSubtractAvailable: Boolean): SendUiState { val state = currentStateProvider() val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO val feeState = state.feeState ?: return state @@ -65,35 +58,46 @@ internal class FeeStateFactory( val fee = feeSelectorState.getFee() val receivedAmount = calculateReceiveAmount(state, fee) - val updatedState = feeState.copy( - feeSelectorState = feeSelectorState, - fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - isSubtract = checkAutoSubtract(state, fee, balance), - ) return state.copy( - feeState = updatedState.copy( - notifications = feeNotificationFactory( - feeState = updatedState, - amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, - ), - isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), + feeState = feeState.copy( + isSubtractAvailable = isSubtractAvailable, + feeSelectorState = feeSelectorState, + fee = fee, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = isSubtractAvailable && checkAutoSubtract(state, fee, balance), + ), + ) + } + + fun onFeeOnLoadedState(fees: TransactionFee): SendUiState { + val state = currentStateProvider() + val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val feeState = state.feeState ?: return state + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state + + val updatedFeeSelector = feeSelectorState.copy( + fees = fees, + customValues = customFeeFieldConverter.convert(fees.normal), + ) + val fee = updatedFeeSelector.getFee() + val receivedAmount = calculateReceiveAmount(state, fee) + return state.copy( + feeState = feeState.copy( + feeSelectorState = updatedFeeSelector, + fee = fee, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = checkAutoSubtract(state, fee, balance), ), ) } fun onFeeOnErrorState(): SendUiState { val state = currentStateProvider() - val updatedState = state.feeState?.copy( - feeSelectorState = FeeSelectorState.Error, - ) return state.copy( - feeState = updatedState?.copy( - notifications = feeNotificationFactory( - feeState = updatedState, - amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, - ), + feeState = state.feeState?.copy( + feeSelectorState = FeeSelectorState.Error, ), ) } @@ -107,21 +111,13 @@ internal class FeeStateFactory( val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType) val fee = updatedFeeSelectorState.getFee() val receivedAmount = calculateReceiveAmount(state, fee) - val updatedState = feeState.copy( - fee = fee, - feeSelectorState = updatedFeeSelectorState, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - isSubtract = checkAutoSubtract(state, fee, balance), - ) - return state.copy( - feeState = updatedState.copy( - notifications = feeNotificationFactory( - feeState = updatedState, - amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, - ), - isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), + feeState = feeState.copy( + fee = fee, + feeSelectorState = updatedFeeSelectorState, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = checkAutoSubtract(state, fee, balance), ), ) } @@ -139,20 +135,13 @@ internal class FeeStateFactory( val fee = updatedFeeSelectorState.getFee() val receivedAmount = calculateReceiveAmount(state, fee) - val updatedState = feeState.copy( - feeSelectorState = updatedFeeSelectorState, - fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - isSubtract = checkAutoSubtract(state, fee, balance), - ) return state.copy( - feeState = updatedState.copy( - notifications = feeNotificationFactory( - feeState = updatedState, - amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, - ), - isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), + feeState = feeState.copy( + feeSelectorState = updatedFeeSelectorState, + fee = fee, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = checkAutoSubtract(state, fee, balance), ), ) } @@ -163,42 +152,47 @@ internal class FeeStateFactory( val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state val fee = feeSelectorState.getFee() val receivedAmount = calculateReceiveAmount(state, fee) - val updatedState = feeState.copy( - isSubtract = value, - isUserSubtracted = true, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - fee = fee, - ) return state.copy( - feeState = updatedState.copy( - notifications = feeNotificationFactory( - feeState = updatedState, - amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, - ), - isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), + feeState = feeState.copy( + isSubtract = value, + isUserSubtracted = true, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + fee = fee, ), ) } - private fun SendStates.FeeState.isPrimaryButtonEnabled(): Boolean { - return when (feeSelectorState) { - is FeeSelectorState.Content -> { - val customValue = feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull() - val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO - val fee = feeSelectorState.getFee() - val feeValue = fee.amount.value ?: BigDecimal.ZERO + fun getFeeNotificationState(notifications: ImmutableList): SendUiState { + val state = currentStateProvider() + return state.copy( + feeState = state.feeState?.copy( + notifications = notifications, + isPrimaryButtonEnabled = isPrimaryButtonEnabled(state.feeState, notifications), + ), + ) + } - val isNotCustom = feeSelectorState.selectedFee != FeeType.CUSTOM - val isNotEmptyCustom = !customValue.isNullOrZero() && !isNotCustom - val isFiatAnotherCurrency = cryptoCurrencyStatusProvider().currency.symbol != fee.amount.currencySymbol - val isSubtractRequired = if (feeValue + receivedAmountValue >= balance) isSubtract else true - val isBalanceEnough = feeValue + receivedAmountValue <= balance + private fun isPrimaryButtonEnabled( + feeState: SendStates.FeeState, + notifications: ImmutableList, + ): Boolean { + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return false + val customValue = feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull() + val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val fee = feeSelectorState.getFee() + val feeValue = fee.amount.value ?: BigDecimal.ZERO - (isFiatAnotherCurrency || isBalanceEnough && isSubtractRequired) && (isNotEmptyCustom || isNotCustom) - } - else -> false + val isNotCustom = feeSelectorState.selectedFee != FeeType.CUSTOM + val isNotEmptyCustom = !customValue.isNullOrZero() && !isNotCustom + val noErrors = notifications.none { it is SendFeeNotification.Error } + val isSubtractRequired = when { + !feeState.isSubtractAvailable -> true // current currency is not fee currency + feeValue + feeState.receivedAmountValue >= balance -> feeState.isSubtract + else -> feeValue + feeState.receivedAmountValue <= balance } + + return noErrors && isSubtractRequired && (isNotEmptyCustom || isNotCustom) } private fun checkAutoSubtract(state: SendUiState, fee: Fee, balance: BigDecimal): Boolean { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt index ccd5f4ae89..312ed32c90 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -10,6 +10,7 @@ internal class SendFeeStateConverter : Converter { override fun convert(value: Unit): SendStates.FeeState { return SendStates.FeeState( feeSelectorState = FeeSelectorState.Loading, + isSubtractAvailable = false, isSubtract = false, isUserSubtracted = false, fee = null, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt index c490f4b84b..43ab51b3ac 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -22,6 +22,7 @@ import kotlinx.collections.immutable.ImmutableList private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY" private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY" +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: SendClickIntents) { if (state == null) return @@ -41,6 +42,7 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S SendSpeedSelector( state = feeSendState, clickIntents = clickIntents, + modifier = Modifier.animateItemPlacement(), ) } customFee(feeSendState) @@ -48,6 +50,7 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S subtractButton( receivedAmount = state.receivedAmount, isSubtract = state.isSubtract, + isSubtractAvailable = state.isSubtractAvailable, clickIntents = clickIntents, ) } @@ -73,6 +76,13 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.icon.accent + is SendFeeNotification.Error.ExceedsBalance -> { + if (it.config.buttonsState == null) { + TangemTheme.colors.icon.warning + } else { + null + } + } else -> null }, ) @@ -108,17 +118,20 @@ internal fun LazyListScope.customFee(feeSendState: FeeSelectorState, modifier: M internal fun LazyListScope.subtractButton( receivedAmount: String, isSubtract: Boolean, + isSubtractAvailable: Boolean, clickIntents: SendClickIntents, modifier: Modifier = Modifier, ) { - item { - SendSpeedSubtract( - receivingAmount = receivedAmount, - isSubtract = isSubtract, - onSelectClick = clickIntents::onSubtractSelect, - modifier = modifier - .padding(vertical = TangemTheme.dimens.spacing12) - .animateItemPlacement(), - ) + if (isSubtractAvailable) { + item { + SendSpeedSubtract( + receivingAmount = receivedAmount, + isSubtract = isSubtract, + onSelectClick = clickIntents::onSubtractSelect, + modifier = modifier + .padding(vertical = TangemTheme.dimens.spacing12) + .animateItemPlacement(), + ) + } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 0f5ede3fe2..cbdd0688e0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -17,10 +17,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase -import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase -import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase +import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.utils.convertToAmount @@ -40,10 +37,7 @@ import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.* -import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState -import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory -import com.tangem.features.send.impl.presentation.state.fee.FeeType -import com.tangem.features.send.impl.presentation.state.fee.getFee +import com.tangem.features.send.impl.presentation.state.fee.* import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel @@ -73,8 +67,10 @@ internal class SendViewModel @Inject constructor( private val parseSharedAddressUseCase: ParseSharedAddressUseCase, private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, + private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, validateWalletMemoUseCase: ValidateWalletMemoUseCase, + getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { @@ -106,7 +102,6 @@ internal class SendViewModel @Inject constructor( coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), - userWalletProvider = Provider { userWallet }, ) private val eventStateFactory = SendEventStateFactory( @@ -115,6 +110,15 @@ internal class SendViewModel @Inject constructor( feeStateFactory = feeStateFactory, ) + private val feeNotificationFactory = FeeNotificationFactory( + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, + currentStateProvider = Provider { uiState }, + userWalletProvider = Provider { userWallet }, + clickIntents = this, + getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase, + ) + private val sendNotificationFactory = SendNotificationFactory( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, @@ -129,6 +133,7 @@ internal class SendViewModel @Inject constructor( private set private var userWallet: UserWallet by Delegates.notNull() + private var isAmountSubtractAvailable: Boolean = false private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() @@ -137,6 +142,7 @@ internal class SendViewModel @Inject constructor( private var feeJobHolder = JobHolder() private var addressValidationJobHolder = JobHolder() private var sendNotificationsJobHolder = JobHolder() + private var feeNotificationsJobHolder = JobHolder() private var qrScannerJobHolder = JobHolder() private var checkFeeUpdateScheduler = SingleTaskScheduler?>() @@ -163,6 +169,7 @@ internal class SendViewModel @Inject constructor( getUserWalletUseCase(userWalletId).fold( ifRight = { wallet -> userWallet = wallet + checkIfSubtractAvailable() getCurrenciesStatusUpdates(owner, wallet) }, ifLeft = { @@ -326,6 +333,16 @@ internal class SendViewModel @Inject constructor( .saveIn(sendNotificationsJobHolder) } + private fun updateFeeNotifications() { + feeNotificationFactory.create() + .conflate() + .distinctUntilChanged() + .onEach { uiState = feeStateFactory.getFeeNotificationState(notifications = it) } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(feeNotificationsJobHolder) + } + // region screen state navigation override fun popBackStack() = stateRouter.popBackStack() override fun onBackClick() = stateRouter.onBackClick() @@ -429,30 +446,41 @@ internal class SendViewModel @Inject constructor( override fun onFeeSelectorClick(feeType: FeeType) { uiState = feeStateFactory.onFeeSelectedState(feeType) + updateFeeNotifications() } override fun onCustomFeeValueChange(index: Int, value: String) { uiState = feeStateFactory.onCustomFeeValueChange(index, value) + updateFeeNotifications() } override fun onSubtractSelect(value: Boolean) { uiState = feeStateFactory.onSubtractSelect(value) + updateFeeNotifications() } private fun loadFee() { viewModelScope.launch(dispatchers.main) { uiState = feeStateFactory.onFeeOnLoadingState() uiState = callFeeUseCase()?.fold( - ifRight = { - feeStateFactory.onFeeOnLoadedState(it) + ifRight = { fees -> + feeStateFactory.onFeeOnLoadedState(fees, isAmountSubtractAvailable) }, ifLeft = { feeStateFactory.onFeeOnErrorState() }, ) ?: feeStateFactory.onFeeOnErrorState() + updateFeeNotifications() }.saveIn(feeJobHolder) } + private suspend fun checkIfSubtractAvailable() { + isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, cryptoCurrency).fold( + ifRight = { it }, + ifLeft = { false }, + ) + } + private suspend fun callFeeUseCase(): Either? { val amountState = uiState.amountState ?: return null val recipientState = uiState.recipientState ?: return null @@ -501,8 +529,11 @@ internal class SendViewModel @Inject constructor( val memo = uiState.recipientState?.memoTextField?.value val fee = feeSelectorState.getFee() val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return - - val amountToSend = if (feeState.isSubtract) feeState.receivedAmountValue else amountValue + val amountToSend = if (feeState.isSubtract && isAmountSubtractAvailable) { + feeState.receivedAmountValue + } else { + amountValue + } viewModelScope.launch(dispatchers.main) { createTransactionUseCase(