Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-29 13:38:50 +03:00
parent 2a689bbbe4
commit f8da5c0d58
13 changed files with 455 additions and 66 deletions

View file

@ -447,6 +447,8 @@
<string name="send_amount_label">Сумма</string>
<string name="send_amount_substract">Вычесть из суммы отправки</string>
<string name="send_amount_substract_footer">Сумма к получению %s</string>
<string name="send_alert_transaction_failed_title">Транзакция не выполнена</string>
<string name="send_alert_transaction_failed_text">Причина: %1$s\Код:%2$s</string>
<string name="send_date_format">%1$s в %2$s</string>
<string name="send_destination_hint_address">Адрес</string>
<string name="send_destination_tag_field">Код назначения</string>
@ -481,8 +483,8 @@
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению</string>
<string name="send_notification_invalid_amount_title">Недопустимая сумма</string>
<string name="send_notification_invalid_minimum_amount_text">Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s.</string>
<string name="send_notification_invalid_reserve_amount_text">Пожалуйста, пополните свой баланс, чтобы продолжить</string>
<string name="send_notification_invalid_reserve_amount_title">Сумма резерва не может быть менее %1$s</string>
<string name="send_notification_invalid_reserve_amount_text">Адрес получателя не активирован. Пожалуйста, измените сумму отправки, чтобы продолжить.</string>
<string name="send_notification_invalid_reserve_amount_title">Сумма отправки не может быть менее %1$s</string>
<string name="send_notification_transaction_delay_text">Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции</string>
<string name="send_notification_transaction_delay_title">Возможны задержки по транзакции</string>
<string name="send_optional_field">Необязательное</string>

View file

@ -446,6 +446,8 @@
<string name="send_amount_label">Amount</string>
<string name="send_amount_substract">Subtract from send amount</string>
<string name="send_amount_substract_footer">The recipient will receive %s</string>
<string name="send_alert_transaction_failed_title">The transaction is not completed</string>
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode:%2$s</string>
<string name="send_confirm_label">Confirm</string>
<string name="send_date_format">%1$s at %2$s</string>
<string name="send_destination_hint_address">Address</string>
@ -486,10 +488,14 @@
<string name="send_notification_invalid_amount_text">The included commission exceeds the transfer amount, leading to a negative value</string>
<string name="send_notification_invalid_amount_title">Invalid amount</string>
<string name="send_notification_invalid_minimum_amount_text">The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s.</string>
<string name="send_notification_invalid_reserve_amount_text">Please top up your balance to continue</string>
<string name="send_notification_invalid_reserve_amount_title">The balance amount must be at least %1$s</string>
<string name="send_notification_invalid_reserve_amount_text">Target account is not created. Please change the amount to send.</string>
<string name="send_notification_invalid_reserve_amount_title">The amount to send must be at least %1$s</string>
<string name="send_notification_transaction_delay_text">Kindly be aware that your transaction may experience delays under specific fee settings</string>
<string name="send_notification_transaction_delay_title">Transaction delays are possible</string>
<string name="send_notifiaction_transaction_limit_title">Transaction limitation</string>
<string name="send_notifiaction_transaction_limit_text">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.</string>
<string name="send_notification_existential_deposit_title">Existential deposit</string>
<string name="send_notification_existential_deposit_text">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.</string>
<string name="send_optional_field">Optional</string>
<string name="send_qrcode_alert_change">Change</string>
<string name="send_qrcode_alert_decline">Decline</string>

View file

@ -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<TxHistoryItem> {
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<WalletManager> {
return walletManagersStore.getAllSync(userWalletId)
}
@Deprecated(
"Use NetworkAddress from CryptoCurrencyStatus",
ReplaceWith("cryptoCurrencyStatus.value.networkAddress"),
)
override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List<Address> {
return getAddresses(userWalletId, network).sortedBy { it.type }
}
@Deprecated("Use NetworkAddress from CryptoCurrencyStatus")
override suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set<Address> {
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<List<WalletManager>> {
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<TransactionFee>? {
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<TransactionError>? {
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,
)
}
}

View file

@ -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<List<WalletManager>>

View file

@ -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()
}

View file

@ -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)),
)
}
}

View file

@ -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<CryptoCurrencyStatus>,
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val currentStateProvider: Provider<SendUiState>,
private val userWalletProvider: Provider<UserWallet>,
private val walletManagersFacade: WalletManagersFacade,
) {
fun create(): Flow<ImmutableList<SendNotification>> = 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<SendNotification>.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<SendNotification>.addInvalidAmountNotification(
isSubtractAmount: Boolean,
receivedAmount: BigDecimal,
) {
if (isSubtractAmount && receivedAmount <= BigDecimal.ZERO) {
add(SendNotification.Error.InvalidAmount)
}
}
private fun MutableList<SendNotification>.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<SendNotification>.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<SendNotification>.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<SendNotification>.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<SendNotification>.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"
}
}

View file

@ -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<SendNotification>): SendUiState {
val state = currentStateProvider()
val hasErrorNotifications = notifications.any { it is SendNotification.Error }
return state.copy(
sendState = state.sendState.copy(
isPrimaryButtonEnabled = !hasErrorNotifications,
notifications = notifications,
),
)
}
//endregion
}

View file

@ -86,6 +86,7 @@ internal sealed class SendStates {
val isSuccess: Boolean = false,
val transactionDate: Long = 0L,
val txUrl: String = "",
val notifications: ImmutableList<SendNotification> = persistentListOf(),
) : SendStates()
}

View file

@ -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()
}
}
/**

View file

@ -187,6 +187,7 @@ private fun isButtonEnabled(currentState: State<SendUiStateType>, 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
}
}

View file

@ -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<SendNotification>, 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
},
)
},
)
}

View file

@ -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(