diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 254dd85c1e..e29baf80c0 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -447,6 +447,8 @@ Сумма Вычесть из суммы отправки Сумма к получению %s + Транзакция не выполнена + Причина: %1$s\Код:%2$s %1$s в %2$s Адрес Код назначения @@ -481,8 +483,8 @@ Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению Недопустимая сумма Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s. - Пожалуйста, пополните свой баланс, чтобы продолжить - Сумма резерва не может быть менее %1$s + Адрес получателя не активирован. Пожалуйста, измените сумму отправки, чтобы продолжить. + Сумма отправки не может быть менее %1$s Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции Возможны задержки по транзакции Необязательное diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index f4187be85b..0186ecb9ff 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -446,6 +446,8 @@ Amount Subtract from send amount The recipient will receive %s + The transaction is not completed + Reason: %1$s\nCode:%2$s Confirm %1$s at %2$s Address @@ -486,10 +488,14 @@ The included commission exceeds the transfer amount, leading to a negative value Invalid amount The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s. - Please top up your balance to continue - The balance amount must be at least %1$s + Target account is not created. Please change the amount to send. + The amount to send must be at least %1$s Kindly be aware that your transaction may experience delays under specific fee settings Transaction delays are possible + Transaction limitation + Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. + Existential deposit + The account will be wiped from the blockchain if a balance goes below the existential deposit. Please ensure that the remaining balance after sending will not be less than %s. Optional Change Decline diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 1eaefab578..c29a7a5b33 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -145,11 +145,9 @@ class DefaultWalletManagersFacade( contractAddress: String?, ): String { val blockchain = Blockchain.fromId(network.id.value) - val walletManager = getOrCreateWalletManager( userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = network.derivationPath.value, + network = network, ) requireNotNull(walletManager) { @@ -165,15 +163,13 @@ class DefaultWalletManagersFacade( } override suspend fun getTxHistoryState(userWalletId: UserWalletId, currency: CryptoCurrency): TxHistoryState { - val blockchain = Blockchain.fromId(currency.network.id.value) val walletManager = getOrCreateWalletManager( userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = currency.network.derivationPath.value, + network = currency.network, ) requireNotNull(walletManager) { - "Unable to get a wallet manager for blockchain: $blockchain" + "Unable to get a wallet manager for blockchain: ${currency.network}" } return walletManager @@ -193,15 +189,13 @@ class DefaultWalletManagersFacade( page: Int, pageSize: Int, ): PaginationWrapper { - val blockchain = Blockchain.fromId(currency.network.id.value) val walletManager = getOrCreateWalletManager( userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = currency.network.derivationPath.value, + network = currency.network, ) requireNotNull(walletManager) { - "Unable to get a wallet manager for blockchain: $blockchain" + "Unable to get a wallet manager for blockchain: ${currency.network}" } val itemsResult = walletManager.getTransactionsHistory( @@ -301,6 +295,7 @@ class DefaultWalletManagersFacade( } } + @Deprecated("Will be removed in future") override suspend fun getOrCreateWalletManager( userWalletId: UserWalletId, blockchain: Blockchain, @@ -326,33 +321,33 @@ class DefaultWalletManagersFacade( return walletManager } + @Deprecated("Will be removed in future") override suspend fun getStoredWalletManagers(userWalletId: UserWalletId): List { return walletManagersStore.getAllSync(userWalletId) } + @Deprecated( + "Use NetworkAddress from CryptoCurrencyStatus", + ReplaceWith("cryptoCurrencyStatus.value.networkAddress"), + ) override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
{ return getAddresses(userWalletId, network).sortedBy { it.type } } + @Deprecated("Use NetworkAddress from CryptoCurrencyStatus") override suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set
{ - val blockchain = Blockchain.fromId(network.id.value) - - return getOrCreateWalletManager( + val manager = getOrCreateWalletManager( userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = network.derivationPath.value, + network = network, ) - ?.wallet - ?.addresses - .orEmpty() + + return manager?.wallet?.addresses.orEmpty() } override suspend fun getRentInfo(userWalletId: UserWalletId, network: Network): CryptoCurrencyWarning.Rent? { - val blockchain = Blockchain.fromId(network.id.value) val manager = getOrCreateWalletManager( userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = network.derivationPath.value, + network = network, ) if (manager !is RentProvider) return null @@ -381,17 +376,52 @@ class DefaultWalletManagersFacade( } } + @Deprecated("Will be removed in future") override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? { - val blockchain = Blockchain.fromId(network.id.value) val manager = getOrCreateWalletManager( userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = network.derivationPath.value, + network = network, ) return if (manager is ExistentialDepositProvider) manager.getExistentialDeposit() else null } + @Deprecated("Will be removed in future") + override suspend fun getReserveAmount(userWalletId: UserWalletId, network: Network): BigDecimal? { + val manager = getOrCreateWalletManager( + userWalletId = userWalletId, + network = network, + ) + + return if (manager is ReserveAmountProvider) manager.getReserveAmount() else null + } + + @Deprecated("Will be removed in future") + override suspend fun checkIfAccountFunded(userWalletId: UserWalletId, network: Network, address: String): Boolean { + val manager = getOrCreateWalletManager( + userWalletId = userWalletId, + network = network, + ) + + return if (manager is ReserveAmountProvider) manager.isAccountFunded(address) else true + } + + @Deprecated("Will be removed in future") + override suspend fun checkUtxoAmountLimit( + userWalletId: UserWalletId, + network: Network, + amount: BigDecimal, + fee: BigDecimal, + ): UtxoAmountLimit? { + val manager = getOrCreateWalletManager( + userWalletId = userWalletId, + network = network, + ) + + return if (manager is UtxoAmountLimitProvider) manager.checkUtxoAmountLimit(amount, fee) else null + } + + @Deprecated("Will be removed in future") override fun getAll(userWalletId: UserWalletId): Flow> { return walletManagersStore.getAll(userWalletId) } @@ -404,8 +434,7 @@ class DefaultWalletManagersFacade( return either { val walletManager = getOrCreateWalletManager( userWalletId = userWalletId, - blockchain = Blockchain.fromId(network.id.value), - derivationPath = network.derivationPath.value, + network = network, ) val validator = ensureNotNull(walletManager as? SignatureCountValidator) { @@ -419,17 +448,16 @@ class DefaultWalletManagersFacade( } } + @Deprecated("Will be removed in future") override suspend fun getFee( amount: Amount, destination: String, userWalletId: UserWalletId, network: Network, ): Result? { - val blockchain = Blockchain.fromId(network.id.value) val walletManager = getOrCreateWalletManager( userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = network.derivationPath.value, + network = network, ) return (walletManager as? TransactionSender)?.getFee( amount = amount, @@ -457,21 +485,21 @@ class DefaultWalletManagersFacade( ) } + @Deprecated("Will be removed in future") override suspend fun validateTransaction( amount: Amount, fee: Amount?, userWalletId: UserWalletId, network: Network, ): EnumSet? { - val blockchain = Blockchain.fromId(network.id.value) val walletManager = getOrCreateWalletManager( userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = network.derivationPath.value, + network = network, ) return walletManager?.validateTransaction(amount, fee) } + @Deprecated("Will be removed in future") override suspend fun createTransaction( amount: Amount, fee: Fee, @@ -480,27 +508,24 @@ class DefaultWalletManagersFacade( userWalletId: UserWalletId, network: Network, ): TransactionData? { - val blockchain = Blockchain.fromId(network.id.value) val walletManager = getOrCreateWalletManager( userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = network.derivationPath.value, + network = network, ) return walletManager?.createTransaction(amount, fee, destination) } + @Deprecated("Will be removed in future") override suspend fun sendTransaction( txData: TransactionData, signer: CommonSigner, userWalletId: UserWalletId, network: Network, ): SimpleResult { - val blockchain = Blockchain.fromId(network.id.value) val walletManager = getOrCreateWalletManager( userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = network.derivationPath.value, + network = network, ) return (walletManager as TransactionSender).send(txData, signer) } @@ -537,4 +562,13 @@ class DefaultWalletManagersFacade( walletManager.addTokens(tokensToAdd) } + + private suspend fun getOrCreateWalletManager(userWalletId: UserWalletId, network: Network): WalletManager? { + val blockchain = Blockchain.fromId(network.id.value) + return getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 922c354dde..d8647329a8 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -139,9 +139,41 @@ interface WalletManagersFacade { /** * Returns value which indicates if the account balance drops below the existential deposit value, it will be * deactivated and any remaining funds will be destroyed. + * + * [REDACTED_TODO_COMMENT] */ + @Deprecated("Will be removed in future") suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? + /** + * Returns reserve amount which is required to create an account + * + * [REDACTED_TODO_COMMENT] + */ + @Deprecated("Will be removed in future") + suspend fun getReserveAmount(userWalletId: UserWalletId, network: Network): BigDecimal? + + /** + * Returns true if account with [address] was reserved with minimum amount + * + * [REDACTED_TODO_COMMENT] + */ + @Deprecated("Will be removed in future") + suspend fun checkIfAccountFunded(userWalletId: UserWalletId, network: Network, address: String): Boolean + + /** + * Checks if transaction amount is within the UTXO limit + * + * [REDACTED_TODO_COMMENT] + */ + @Deprecated("Will be removed in future") + suspend fun checkUtxoAmountLimit( + userWalletId: UserWalletId, + network: Network, + amount: BigDecimal, + fee: BigDecimal, + ): UtxoAmountLimit? + @Deprecated("Will be removed in future") fun getAll(userWalletId: UserWalletId): Flow> diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt deleted file mode 100644 index 720270d86d..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.send.impl.presentation.domain - -sealed class SendNotification { - - sealed class Info(val message: String) : SendNotification() - - sealed class Critical(val message: String) : SendNotification() - - sealed class Error(val message: String) : SendNotification() -} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt new file mode 100644 index 0000000000..6d2975319a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -0,0 +1,77 @@ +package com.tangem.features.send.impl.presentation.state + +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.features.send.impl.R + +internal sealed class SendNotification(val config: NotificationConfig) { + + sealed class Error( + title: TextReference, + subtitle: TextReference, + buttonState: NotificationConfig.ButtonsState? = null, + ) : SendNotification( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = R.drawable.ic_alert_24, + buttonsState = buttonState, + ), + ) { + + object TotalExceedsBalance : Error( + title = resourceReference(R.string.send_notification_exceed_balance_title), + subtitle = resourceReference(R.string.send_notification_exceed_balance_text), + ) + + object InvalidAmount : Error( + title = resourceReference(R.string.send_notification_invalid_amount_title), + subtitle = resourceReference(R.string.send_notification_invalid_amount_text), + ) + + data class MinimumAmountError(val amount: String) : Error( + title = resourceReference(R.string.send_notification_invalid_amount_title), + subtitle = resourceReference(R.string.send_notification_invalid_minimum_amount_text, wrappedList(amount)), + ) + + data class ReserveAmountError(val amount: String) : Error( + title = resourceReference(R.string.send_notification_invalid_reserve_amount_title, wrappedList(amount)), + subtitle = resourceReference(R.string.send_notification_invalid_reserve_amount_text), + ) + + data class TransactionLimitError( + val cryptoCurrency: String, + val utxoLimit: String, + val amountLimit: String, + ) : Error( + title = resourceReference(R.string.send_notifiaction_transaction_limit_title), + subtitle = resourceReference( + R.string.send_notifiaction_transaction_limit_text, + wrappedList(cryptoCurrency, utxoLimit, amountLimit), + ), + ) + } + + sealed class Warning( + title: TextReference, + subtitle: TextReference, + ) : SendNotification( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = R.drawable.img_attention_20, + ), + ) { + data class HighFeeError(val amount: String) : Warning( + title = resourceReference(R.string.send_notification_high_fee_title), + subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)), + ) + + data class ExistentialDeposit(val deposit: String) : Warning( + title = resourceReference(R.string.send_notification_existential_deposit_title), + subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt new file mode 100644 index 0000000000..ce29bd2c60 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt @@ -0,0 +1,188 @@ +package com.tangem.features.send.impl.presentation.state + +import com.tangem.blockchain.common.Blockchain +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.utils.Provider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import java.math.BigDecimal + +internal class SendNotificationFactory( + private val cryptoCurrencyStatusProvider: Provider, + private val coinCryptoCurrencyStatusProvider: Provider, + private val currentStateProvider: Provider, + private val userWalletProvider: Provider, + private val walletManagersFacade: WalletManagersFacade, +) { + + fun create(): Flow> = currentStateProvider().currentState + .filter { it == SendUiStateType.Send } + .map { + val state = currentStateProvider() + val feeState = state.feeState ?: return@map persistentListOf() + val recipientState = state.recipientState ?: return@map persistentListOf() + val feeAmount = feeState.fee?.amount?.value ?: BigDecimal.ZERO + buildList { + // errors + addExceedBalanceNotification(feeAmount, feeState.receivedAmountValue) + addInvalidAmountNotification(feeState.isSubtract, feeState.receivedAmountValue) + addMinimumAmountErrorNotification(feeAmount, feeState.receivedAmountValue) + addReserveAmountErrorNotification(recipientState.addressTextField.value) + addTransactionLimitErrorNotification(feeAmount, feeState.receivedAmountValue) + // warnings + addExistentialWarningNotification(feeAmount, feeState.receivedAmountValue) + addHighFeeWarningNotification() + }.toImmutableList() + } + + private fun MutableList.addExceedBalanceNotification( + feeAmount: BigDecimal, + receivedAmount: BigDecimal, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider() + val cryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + val coinCryptoAmount = coinCryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + + val showNotification = if (cryptoCurrencyStatus.currency is CryptoCurrency.Token) { + receivedAmount > cryptoAmount || feeAmount > coinCryptoAmount + } else { + receivedAmount + feeAmount > cryptoAmount + } + + if (showNotification) { + add(SendNotification.Error.TotalExceedsBalance) + } + } + + private fun MutableList.addInvalidAmountNotification( + isSubtractAmount: Boolean, + receivedAmount: BigDecimal, + ) { + if (isSubtractAmount && receivedAmount <= BigDecimal.ZERO) { + add(SendNotification.Error.InvalidAmount) + } + } + + private fun MutableList.addMinimumAmountErrorNotification( + feeAmount: BigDecimal, + receivedAmount: BigDecimal, + ) { + val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider() + + val totalAmount = feeAmount + receivedAmount + val balance = coinCryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + + // TODO Move Blockchain check elsewhere + when (coinCryptoCurrencyStatus.currency.network.id.value) { + Blockchain.Cardano.id -> { + if (receivedAmount > BigDecimal.ONE || balance - totalAmount < BigDecimal.ONE) { + add(SendNotification.Error.MinimumAmountError(CARDANO_MINIMUM)) + } + } + Blockchain.Dogecoin.id -> { + val minimum = BigDecimal(DOGECOIN_MINIMUM) + if (receivedAmount > minimum || balance - totalAmount < minimum) { + add(SendNotification.Error.MinimumAmountError(DOGECOIN_MINIMUM)) + } + } + else -> Unit + } + } + + private suspend fun MutableList.addReserveAmountErrorNotification(recipientAddress: String) { + val userWalletId = userWalletProvider().walletId + val cryptoCurrency = cryptoCurrencyStatusProvider().currency + val isAccountFunded = walletManagersFacade.checkIfAccountFunded( + userWalletId, + cryptoCurrency.network, + recipientAddress, + ) + val minimumAmount = walletManagersFacade.getReserveAmount(userWalletId, cryptoCurrency.network) + if (!isAccountFunded && minimumAmount != null && minimumAmount > BigDecimal.ZERO) { + add( + SendNotification.Error.ReserveAmountError( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = minimumAmount, + cryptoCurrency = cryptoCurrency, + ), + ), + ) + } + } + + private suspend fun MutableList.addTransactionLimitErrorNotification( + feeAmount: BigDecimal, + receivedAmount: BigDecimal, + ) { + val userWalletId = userWalletProvider().walletId + val cryptoCurrency = cryptoCurrencyStatusProvider().currency + val utxoLimit = walletManagersFacade.checkUtxoAmountLimit( + userWalletId = userWalletId, + network = cryptoCurrency.network, + amount = receivedAmount, + fee = feeAmount, + ) + + if (utxoLimit != null) { + add( + SendNotification.Error.TransactionLimitError( + cryptoCurrency = cryptoCurrency.name, + utxoLimit = utxoLimit.maxLimit.toPlainString(), + amountLimit = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = utxoLimit.maxAmount, + cryptoCurrency = cryptoCurrency, + ), + ), + ) + } + } + + private suspend fun MutableList.addExistentialWarningNotification( + feeAmount: BigDecimal, + receivedAmount: BigDecimal, + ) { + val userWalletId = userWalletProvider().walletId + val cryptoCurrency = cryptoCurrencyStatusProvider().currency + val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) { + feeAmount + } else { + feeAmount + receivedAmount + } + val currencyDeposit = walletManagersFacade.getExistentialDeposit( + userWalletId, + cryptoCurrency.network, + ) + if (currencyDeposit != null && currencyDeposit > spendingAmount) { + add( + SendNotification.Warning.ExistentialDeposit( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = currencyDeposit, + cryptoCurrency = cryptoCurrency, + ), + ), + ) + } + } + + private fun MutableList.addHighFeeWarningNotification() { + // TODO Move Blockchain check elsewhere + if (cryptoCurrencyStatusProvider().currency.network.id.value == Blockchain.Tezos.id) { + add(SendNotification.Warning.HighFeeError(TEZOS_FEE_THRESHOLD)) + } + } + + companion object { + private const val CARDANO_MINIMUM = "1" + private const val DOGECOIN_MINIMUM = "0.01" + private const val TEZOS_FEE_THRESHOLD = "0.01" + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 608acd39e8..177b470fda 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -24,6 +24,7 @@ import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientS 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 kotlinx.coroutines.flow.MutableStateFlow @@ -238,7 +239,7 @@ internal class SendStateFactory( ) val fee = feeSelectorState.getFee() - val receivedAmount = calculateReceiveAmount(state, fee) + val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract) val updatedState = feeState.copy( feeSelectorState = feeSelectorState, fee = fee, @@ -260,7 +261,7 @@ internal class SendStateFactory( val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType) val fee = updatedFeeSelectorState.getFee() - val receivedAmount = calculateReceiveAmount(state, fee) + val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract) val updatedState = feeState.copy( fee = fee, @@ -288,7 +289,7 @@ internal class SendStateFactory( ) val fee = updatedFeeSelectorState.getFee() - val receivedAmount = calculateReceiveAmount(state, fee) + val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract) val updatedState = feeState.copy( feeSelectorState = updatedFeeSelectorState, @@ -310,7 +311,7 @@ internal class SendStateFactory( val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state val fee = feeSelectorState.getFee() - val receivedAmount = calculateReceiveAmount(state, fee) + val receivedAmount = calculateReceiveAmount(state, fee, value) val updatedState = feeState.copy( isSubtract = value, fee = fee, @@ -375,5 +376,16 @@ internal class SendStateFactory( ), ) } + + fun getSendNotificationState(notifications: ImmutableList): SendUiState { + val state = currentStateProvider() + val hasErrorNotifications = notifications.any { it is SendNotification.Error } + return state.copy( + sendState = state.sendState.copy( + isPrimaryButtonEnabled = !hasErrorNotifications, + notifications = notifications, + ), + ) + } //endregion } \ 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 a64cbfa738..fb2918c11d 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 @@ -86,6 +86,7 @@ internal sealed class SendStates { val isSuccess: Boolean = false, val transactionDate: Long = 0L, val txUrl: String = "", + val notifications: ImmutableList = persistentListOf(), ) : SendStates() } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt index dfbbab99e8..1805ce3f6a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt @@ -8,10 +8,14 @@ import java.math.BigDecimal /** * Calculate receiving amount when fee is subtracted from sending amount */ -internal fun calculateReceiveAmount(uiState: SendUiState, feeAmount: Fee): BigDecimal { +internal fun calculateReceiveAmount(uiState: SendUiState, feeAmount: Fee, isSubtract: Boolean): BigDecimal { val amount = uiState.amountState?.amountTextField?.value ?: return BigDecimal.ZERO val fee = feeAmount.amount.value ?: return BigDecimal.ZERO - return BigDecimal(amount).minus(fee) + return if (isSubtract) { + amount.toBigDecimal().minus(fee) + } else { + amount.toBigDecimal() + } } /** diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 9b11c51df3..525578a157 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -187,6 +187,7 @@ private fun isButtonEnabled(currentState: State, uiState: SendU SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled ?: false SendUiStateType.Fee -> uiState.feeState?.isPrimaryButtonEnabled ?: false + SendUiStateType.Send -> uiState.sendState.isPrimaryButtonEnabled else -> true } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index 479fc9843a..8bb21e14d3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -1,10 +1,13 @@ package com.tangem.features.send.impl.presentation.ui.send import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -18,14 +21,17 @@ import com.tangem.blockchain.extensions.toBigDecimalOrDefault import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.components.inputrow.InputRowImage import com.tangem.core.ui.components.inputrow.InputRowRecipientDefault +import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.SendUiState +import kotlinx.collections.immutable.ImmutableList @Suppress("LongMethod") @Composable @@ -75,6 +81,7 @@ internal fun SendContent(uiState: SendUiState) { ) } } + notifications(sendState.notifications) } } @@ -178,4 +185,24 @@ private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: .background(TangemTheme.colors.background.action) .clickable(enabled = !isSuccess) { onClick() }, ) +} + +@OptIn(ExperimentalFoundationApi::class) +internal fun LazyListScope.notifications(configs: ImmutableList, modifier: Modifier = Modifier) { + items( + items = configs, + key = { it::class.java }, + contentType = { it::class.java }, + itemContent = { + Notification( + config = it.config, + modifier = modifier.animateItemPlacement(), + containerColor = TangemTheme.colors.button.disabled, + iconTint = when (it) { + is SendNotification.Error -> TangemTheme.colors.icon.warning + is SendNotification.Warning -> null + }, + ) + }, + ) } \ 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 36e8b54f06..394c03f652 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 @@ -96,6 +96,14 @@ internal class SendViewModel @Inject constructor( getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase, ) + private val sendNotificationFactory = SendNotificationFactory( + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, + currentStateProvider = Provider { uiState }, + userWalletProvider = Provider { userWallet }, + walletManagersFacade = walletManagersFacade, + ) + var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState()) private set @@ -107,6 +115,7 @@ internal class SendViewModel @Inject constructor( private var recipientsJobHolder = JobHolder() private var feeJobHolder = JobHolder() private var addressValidationJobHolder = JobHolder() + private var sendNotificationsJobHolder = JobHolder() override fun onCreate(owner: LifecycleOwner) { subscribeOnCurrencyStatusUpdates(owner) @@ -146,6 +155,7 @@ internal class SendViewModel @Inject constructor( coinCryptoCurrencyStatus = it getWalletsAndRecent() uiState = stateFactory.getReadyState() + updateNotifications() } } .flowOn(dispatchers.main) @@ -300,6 +310,16 @@ internal class SendViewModel @Inject constructor( }.saveIn(feeJobHolder) } + private fun updateNotifications() { + sendNotificationFactory.create() + .conflate() + .distinctUntilChanged() + .onEach { uiState = stateFactory.getSendNotificationState(notifications = it) } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + .saveIn(sendNotificationsJobHolder) + } + // region screen state navigation override fun popBackStack() = stateRouter.popBackStack() override fun onBackClick() = stateRouter.onBackClick() @@ -376,7 +396,7 @@ internal class SendViewModel @Inject constructor( } return false } -// endregion + // endregion // region fee override fun onFeeSelectorClick(feeType: FeeType) { @@ -410,18 +430,13 @@ internal class SendViewModel @Inject constructor( override fun onExploreClick(txUrl: String) = innerRouter.openUrl(txUrl) private suspend fun verifyAndSendTransaction() { - val amount = uiState.amountState?.amountTextField?.value ?: return val recipient = uiState.recipientState?.addressTextField?.value ?: return val feeState = uiState.feeState ?: return val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return val memo = uiState.recipientState?.memoTextField?.value val fee = feeSelectorState.getFee() - val amountToSend = if (feeState.isSubtract) { - feeState.receivedAmountValue.convertToAmount(cryptoCurrency) - } else { - amount.toBigDecimal().convertToAmount(cryptoCurrency) - } + val amountToSend = feeState.receivedAmountValue.convertToAmount(cryptoCurrency) // todo add error handling [[REDACTED_JIRA]] // val transactionErrors = walletManagersFacade.validateTransaction(