Updated on 2026-08-14

This commit is contained in:
Tangem 2024-04-23 15:56:47 +01:00
parent 385cc79a32
commit a3f2c421bf
69 changed files with 749 additions and 542 deletions

View file

@ -103,6 +103,10 @@ internal sealed class SendAnalyticEvents(
params = mapOf(TOKEN to token),
)
data object NoticeFeeCoverage : SendAnalyticEvents(
event = "Notice - Network Fee Coverage",
)
/** If error occurs during send transactions */
data class TransactionError(val token: String) : SendAnalyticEvents(
event = "Error - Transaction Rejected",

View file

@ -68,16 +68,6 @@ internal sealed class SendAlertState {
override val confirmButtonText: TextReference = resourceReference(R.string.common_continue)
}
data class FeeCoverage(
override val onConfirmClick: (() -> Unit),
) : SendAlertState() {
override val title: TextReference? = null
override val message: TextReference =
resourceReference(id = R.string.send_alert_fee_coverage_title)
override val confirmButtonText: TextReference =
resourceReference(id = R.string.send_alert_fee_coverage_subract_text)
}
data class ReserveAmount(val amount: String) : SendAlertState() {
override val title: TextReference =
resourceReference(id = R.string.send_notification_invalid_reserve_amount_title, wrappedList(amount))

View file

@ -48,19 +48,6 @@ internal class SendEventStateFactory(
)
}
fun getFeeCoverageAlert(onConsume: () -> Unit): SendUiState {
return currentStateProvider().copy(
event = triggeredEvent(
data = SendEvent.ShowAlert(
SendAlertState.FeeCoverage(
onConfirmClick = clickIntents::onSubtractSelect,
),
),
onConsume = onConsume,
),
)
}
fun getFeeUpdatedAlert(fee: TransactionFee, onConsume: () -> Unit, onFeeNotIncreased: () -> Unit): SendUiState {
val state = currentStateProvider()
val feeSelector = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state
@ -139,9 +126,7 @@ internal class SendEventStateFactory(
return state.copy(
event = triggeredEvent(
data = SendEvent.ShowAlert(
SendAlertState.FeeUnreachableError(
onConfirmClick = { clickIntents.feeReload(true) },
),
SendAlertState.FeeUnreachableError(onConfirmClick = clickIntents::feeReload),
),
onConsume = onConsume,
),

View file

@ -42,11 +42,6 @@ internal sealed class SendNotification(val config: NotificationConfig) {
),
)
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,
@ -153,5 +148,10 @@ internal sealed class SendNotification(val config: NotificationConfig) {
onClick = onRefresh,
),
)
data object FeeCoverageNotification : Warning(
title = resourceReference(R.string.send_network_fee_warning_title),
subtitle = resourceReference(R.string.swapping_network_fee_warning_content),
)
}
}

View file

@ -14,7 +14,6 @@ import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSubtractConverter
import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
@ -48,12 +47,6 @@ internal class SendStateFactory(
appCurrencyProvider = appCurrencyProvider,
)
}
private val amountSubtractConverter by lazy(LazyThreadSafetyMode.NONE) {
SendAmountSubtractConverter(
currentStateProvider = currentStateProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendAmountStateConverter(
appCurrencyProvider = appCurrencyProvider,
@ -117,6 +110,7 @@ internal class SendStateFactory(
recipientState = state.recipientState
?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)),
feeState = state.feeState ?: feeStateConverter.convert(Unit),
sendState = confirmStateConverter.convert(Unit),
isEditingDisabled = true,
cryptoCurrencyName = cryptoCurrencyStatusProvider().currency.name,
)
@ -264,14 +258,6 @@ internal class SendStateFactory(
//endregion
//region send
fun onSubtractSelect(isAmountSubtractAvailable: Boolean): SendUiState {
val state = currentStateProvider()
if (!isAmountSubtractAvailable) return state
return amountSubtractConverter.convert(Unit)
}
fun getSendingStateUpdate(isSending: Boolean): SendUiState {
val state = currentStateProvider()
return state.copy(

View file

@ -48,11 +48,9 @@ internal sealed class SendStates {
val walletBalance: TextReference,
val tokenIconState: TokenIconState,
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
val isSegmentedButtonsEnabled: Boolean,
val amountTextField: SendTextField.AmountField,
val notifications: ImmutableList<SendNotification>,
val appCurrencyCode: String,
val isFeeLoading: Boolean,
val subtractedFee: BigDecimal?,
) : SendStates()
/** Recipient state */
@ -89,7 +87,6 @@ internal sealed class SendStates {
override val isPrimaryButtonEnabled: Boolean = true,
val isSending: Boolean,
val isSuccess: Boolean,
val isSubtract: Boolean,
val transactionDate: Long,
val txUrl: String,
val ignoreAmountReduce: Boolean,

View file

@ -1,12 +1,10 @@
package com.tangem.features.send.impl.presentation.state.amount
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendNotification
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldMaxAmountConverter
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
/**
* Factory to produce amount state for [SendUiState]
@ -43,18 +41,4 @@ internal class AmountStateFactory(
}
fun getOnCurrencyChangedState(isFiat: Boolean) = amountCurrencyConverter.convert(isFiat)
fun getAmountNotificationState(notifications: ImmutableList<SendNotification>): SendUiState {
val state = currentStateProvider()
return state.copy(
amountState = state.amountState?.copy(notifications = notifications),
)
}
fun getOnAmountFeeLoadingCancel(): SendUiState {
val state = currentStateProvider()
return state.copy(
amountState = state.amountState?.copy(isFeeLoading = false),
)
}
}

View file

@ -31,6 +31,7 @@ internal class SendAmountStateConverter(
val status = cryptoCurrencyStatusProvider()
val fiat = formatFiatAmount(status.value.fiatAmount, appCurrency.code, appCurrency.symbol)
val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals)
val noFeeRate = status.value.fiatRate.isNullOrZero()
return SendStates.AmountState(
walletName = userWallet.name,
@ -38,26 +39,24 @@ internal class SendAmountStateConverter(
tokenIconState = iconStateConverter.convert(status),
amountTextField = sendAmountFieldConverter.convert(value),
isPrimaryButtonEnabled = false,
notifications = persistentListOf(),
isFeeLoading = false,
appCurrencyCode = appCurrency.code,
subtractedFee = null,
segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) {
persistentListOf()
} else {
persistentListOf(
SendAmountSegmentedButtonsConfig(
title = stringReference(status.currency.symbol),
iconState = iconStateConverter.convert(status),
isFiat = false,
segmentedButtonConfig = persistentListOf(
SendAmountSegmentedButtonsConfig(
title = stringReference(status.currency.symbol),
iconState = iconStateConverter.convertCustom(
value = status,
forceGrayscale = noFeeRate,
showCustomTokenBadge = false,
),
SendAmountSegmentedButtonsConfig(
title = stringReference(appCurrency.code),
iconUrl = appCurrency.iconSmallUrl,
isFiat = true,
),
)
},
isFiat = false,
),
SendAmountSegmentedButtonsConfig(
title = stringReference(appCurrency.code),
iconUrl = appCurrency.iconSmallUrl,
isFiat = true,
),
),
isSegmentedButtonsEnabled = !noFeeRate,
)
}
}

View file

@ -1,48 +0,0 @@
package com.tangem.features.send.impl.presentation.state.amount
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
internal class SendAmountSubtractConverter(
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<Unit, SendUiState> {
override fun convert(value: Unit): SendUiState {
val state = currentStateProvider()
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val amountState = state.amountState ?: return state
val feeState = state.feeState ?: return state
val feeValue = feeState.fee?.amount?.value ?: return state
val amountTextField = amountState.amountTextField
val amountValue = amountTextField.cryptoAmount.value ?: return state
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val cryptoDecimals = amountTextField.cryptoAmount.decimals
val fiatDecimals = amountTextField.fiatAmount.decimals
val feeDiff = amountState.subtractedFee?.let { feeValue.minus(it) } ?: feeValue
val decimalCryptoValue = amountValue.minus(feeDiff)
if (decimalCryptoValue < BigDecimal.ZERO) return state
val decimalFiatValue = decimalCryptoValue.multiply(fiatRate)
val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals)
val fiatValue = decimalFiatValue.parseBigDecimal(fiatDecimals)
return state.copy(
sendState = state.sendState?.copy(isSubtract = true),
amountState = amountState.copy(
subtractedFee = feeValue,
amountTextField = amountTextField.copy(
value = cryptoValue,
fiatValue = fiatValue,
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
),
),
)
}
}

View file

@ -13,7 +13,6 @@ internal class SendConfirmStateConverter(
isPrimaryButtonEnabled = true,
isSending = false,
isSuccess = false,
isSubtract = false,
transactionDate = 0L,
txUrl = "",
ignoreAmountReduce = false,

View file

@ -18,12 +18,11 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
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.FeeType
import com.tangem.features.send.impl.presentation.state.fee.*
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.lib.crypto.BlockchainUtils.isDogecoin
import com.tangem.lib.crypto.BlockchainUtils.isTezos
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
@ -31,17 +30,16 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import java.math.BigDecimal
import java.math.BigInteger
@Suppress("LongParameterList")
internal class SendNotificationFactory(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feePaidCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val currentStateProvider: Provider<SendUiState>,
private val userWalletProvider: Provider<UserWallet>,
private val currencyChecksRepository: CurrencyChecksRepository,
private val stateRouterProvider: Provider<StateRouter>,
private val isSubtractAvailableProvider: Provider<Boolean>,
private val clickIntents: SendClickIntents,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
@ -53,24 +51,39 @@ internal class SendNotificationFactory(
val state = currentStateProvider()
val sendState = state.sendState ?: return@map persistentListOf()
val feeState = state.feeState ?: return@map persistentListOf()
val feeAmount = feeState.fee?.amount?.value ?: BigDecimal.ZERO
val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO
val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO
val isFeeCoverage = checkFeeCoverage(
isSubtractAvailable = isSubtractAvailableProvider(),
balance = balance,
amountValue = amountValue,
feeValue = feeValue,
)
val sendingAmount = calculateSubtractedAmount(
isFeeCoverage = isFeeCoverage,
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
amountValue = amountValue,
feeValue = feeValue,
)
buildList {
// errors
addExceedBalanceNotification(feeAmount, amountValue)
addFeeUnreachableNotification(feeState.feeSelectorState)
addExceedBalanceNotification(feeValue, sendingAmount)
addExceedsBalanceNotification(feeState.fee)
addMinimumAmountErrorNotification(feeAmount, amountValue)
addDustWarningNotification(feeAmount, amountValue)
addTransactionLimitErrorNotification(feeAmount, amountValue)
addMinimumAmountErrorNotification(feeValue, sendingAmount)
addDustWarningNotification(feeValue, sendingAmount)
addTransactionLimitErrorNotification(feeValue, sendingAmount)
// warnings
addExistentialWarningNotification(feeAmount, amountValue)
addHighFeeWarningNotification(feeAmount, amountValue, sendState.ignoreAmountReduce)
addExistentialWarningNotification(feeValue, amountValue)
addFeeCoverageNotification(isFeeCoverage)
addHighFeeWarningNotification(amountValue, sendState.ignoreAmountReduce)
addTooHighNotification(feeState.feeSelectorState)
addTooLowNotification(feeState)
}.toImmutableList()
}
fun dismissNotificationState(clazz: Class<out SendNotification>): SendUiState {
fun dismissNotificationState(clazz: Class<out SendNotification>, isIgnored: Boolean = false): SendUiState {
val state = currentStateProvider()
val sendState = state.sendState ?: return state
val notificationsToRemove = sendState.notifications.filterIsInstance(clazz)
@ -78,50 +91,33 @@ internal class SendNotificationFactory(
updatedNotifications.removeAll(notificationsToRemove)
return state.copy(
sendState = sendState.copy(
ignoreAmountReduce = true,
ignoreAmountReduce = isIgnored,
notifications = updatedNotifications.toImmutableList(),
),
)
}
private fun MutableList<SendNotification>.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) {
if (feeSelectorState is FeeSelectorState.Error) {
add(SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload))
}
}
private fun MutableList<SendNotification>.addExceedBalanceNotification(
feeAmount: BigDecimal,
receivedAmount: BigDecimal,
) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider()
val feePaidCryptoCurrencyStatus = feePaidCryptoCurrencyStatusProvider()
val cryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val coinCryptoAmount = coinCryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val showNotification = if (cryptoCurrencyStatus.currency.id == feePaidCryptoCurrencyStatus?.currency?.id) {
receivedAmount > cryptoAmount || feeAmount > coinCryptoAmount
} else {
receivedAmount + feeAmount > cryptoAmount
}
if (!isSubtractAvailableProvider()) return
val showNotification = receivedAmount + feeAmount > balance
if (showNotification) {
add(SendNotification.Error.TotalExceedsBalance)
}
}
private fun MutableList<SendNotification>.addMinimumAmountErrorNotification(
feeAmount: BigDecimal,
receivedAmount: BigDecimal,
) {
val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider()
val totalAmount = feeAmount + receivedAmount
val balance = coinCryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
if (isDogecoin(coinCryptoCurrencyStatus.currency.network.id.value)) {
val minimum = BigDecimal(DOGECOIN_MINIMUM)
if (receivedAmount < minimum || balance - totalAmount < minimum) {
add(SendNotification.Error.MinimumAmountError(DOGECOIN_MINIMUM))
}
}
}
// todo temporarily disabling notification
// private suspend fun MutableList<SendNotification>.addReserveAmountErrorNotification(recipientAddress: String) {
// val userWalletId = userWalletProvider().walletId
@ -198,7 +194,7 @@ internal class SendNotificationFactory(
if (currencyDeposit != null && currencyDeposit > diff) {
add(
SendNotification.Error.ExistentialDeposit(
BigDecimalFormatter.formatCryptoAmount(
BigDecimalFormatter.formatCryptoAmountUncapped(
cryptoAmount = currencyDeposit,
cryptoCurrency = cryptoCurrency,
),
@ -207,16 +203,22 @@ internal class SendNotificationFactory(
}
}
private fun MutableList<SendNotification>.addFeeCoverageNotification(sendingAmount: Boolean) {
if (sendingAmount) {
analyticsEventHandler.send(SendAnalyticEvents.NoticeFeeCoverage)
add(SendNotification.Warning.FeeCoverageNotification)
}
}
private fun MutableList<SendNotification>.addHighFeeWarningNotification(
feeAmount: BigDecimal,
sendAmount: BigDecimal,
ignoreAmountReduce: Boolean,
) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val isTezos = cryptoCurrencyStatus.currency.network.id.value == Blockchain.Tezos.id
val isTezos = isTezos(cryptoCurrencyStatus.currency.network.id.value)
val threshold = Blockchain.Tezos.minimalAmount()
val isTotalBalance = feeAmount.plus(sendAmount) >= balance && balance > threshold
val isTotalBalance = sendAmount >= balance && balance > threshold
if (!ignoreAmountReduce && isTotalBalance && isTezos) {
add(
SendNotification.Warning.HighFeeError(
@ -233,6 +235,21 @@ internal class SendNotificationFactory(
}
}
// todo remove in [REDACTED_TASK_KEY]
private fun MutableList<SendNotification>.addMinimumAmountErrorNotification(
feeAmount: BigDecimal,
receivedAmount: BigDecimal,
) {
val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider()
val minimum = BigDecimal(DOGECOIN_MINIMUM)
val isDogecoin = isDogecoin(coinCryptoCurrencyStatus.currency.network.id.value)
val isExceedDustLimit = checkDustLimits(feeAmount, receivedAmount, minimum)
if (isDogecoin && isExceedDustLimit) {
add(SendNotification.Error.MinimumAmountError(DOGECOIN_MINIMUM))
}
}
private suspend fun MutableList<SendNotification>.addDustWarningNotification(
feeAmount: BigDecimal,
receivedAmount: BigDecimal,
@ -241,18 +258,12 @@ internal class SendNotificationFactory(
val dustValue = currencyChecksRepository.getDustValue(
userWalletProvider().walletId,
cryptoCurrencyStatus.currency.network,
)
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
if (dustValue != null && !balance.isNullOrZero() && receivedAmount < balance) {
val totalAmount = feeAmount + receivedAmount
val change = balance - totalAmount
val isChangeLowerThanDust = change < dustValue && change != BigDecimal.ZERO
val isShowWarning = totalAmount < dustValue || isChangeLowerThanDust
if (isShowWarning) {
add(
SendNotification.Error.MinimumAmountError(dustValue.toPlainString()),
)
}
) ?: return
if (checkDustLimits(feeAmount, receivedAmount, dustValue)) {
add(
SendNotification.Error.MinimumAmountError(dustValue.toPlainString()),
)
}
}
@ -274,13 +285,9 @@ internal class SendNotificationFactory(
private fun MutableList<SendNotification>.addTooHighNotification(feeSelectorState: FeeSelectorState) {
if (feeSelectorState !is FeeSelectorState.Content) return
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return
val highValue = multipleFees.priority.amount.value ?: return
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
val diff = (customValue / highValue).toBigInteger()
if (feeSelectorState.selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF) {
add(SendNotification.Warning.TooHigh(diff.toString()))
checkIfFeeTooHigh(feeSelectorState) { diff ->
add(SendNotification.Warning.TooHigh(diff))
}
}
@ -361,8 +368,17 @@ internal class SendNotificationFactory(
return Blockchain.fromNetworkId(this.currency.network.backendId) == Blockchain.Arbitrum
}
private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val totalAmount = feeAmount + receivedAmount
val change = balance - totalAmount
val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO
return receivedAmount < dustValue || isChangeLowerThanDust
}
companion object {
private const val DOGECOIN_MINIMUM = "0.01"
internal val FEE_MAX_DIFF = BigInteger("5")
}
}

View file

@ -1,19 +1,73 @@
package com.tangem.features.send.impl.presentation.state.fee
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchainsdk.utils.minimalAmount
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory
import com.tangem.lib.crypto.BlockchainUtils
import java.math.BigDecimal
import java.math.RoundingMode
/**
* Check if sending amount with fee is greater than balance
* Check and calculates subtracted amount
*/
internal fun checkFeeCoverage(state: SendUiState, cryptoCurrencyStatus: CryptoCurrencyStatus): Boolean {
val balance = cryptoCurrencyStatus.value.amount ?: return false
val fee = state.feeState?.fee?.amount?.value ?: return false
val amount = state.amountState?.amountTextField?.cryptoAmount?.value ?: return false
return balance < amount + fee && balance > fee
internal fun checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountValue: BigDecimal,
feeValue: BigDecimal,
): BigDecimal {
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
val isFeeCoverage = checkFeeCoverage(
isSubtractAvailable = isAmountSubtractAvailable,
balance = balance,
amountValue = amountValue,
feeValue = feeValue,
)
return calculateSubtractedAmount(
isFeeCoverage = isFeeCoverage,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
)
}
/**
* Checks if sending amount with fee is greater than balance
*/
internal fun checkFeeCoverage(
isSubtractAvailable: Boolean,
balance: BigDecimal,
amountValue: BigDecimal,
feeValue: BigDecimal,
): Boolean {
if (!isSubtractAvailable) return false
return balance < amountValue + feeValue && balance > feeValue
}
/**
* Calculates subtracted amount
*/
internal fun calculateSubtractedAmount(
isFeeCoverage: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountValue: BigDecimal,
feeValue: BigDecimal,
): BigDecimal {
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
return if (isFeeCoverage) {
var subtractedValue = minOf(amountValue, balance.minus(feeValue))
if (BlockchainUtils.isTezos(cryptoCurrencyStatus.currency.network.id.value)) {
val threshold = Blockchain.Tezos.minimalAmount()
subtractedValue = -threshold
}
subtractedValue
} else {
amountValue
}
}
/**
@ -32,14 +86,16 @@ internal fun checkIfFeeTooLow(state: SendUiState): Boolean {
/**
* Check if custom fee is too high
*/
internal fun checkIfFeeTooHigh(state: SendUiState, onShow: (String) -> Unit): Boolean {
val feeSelectorState = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false
internal fun checkIfFeeTooHigh(feeSelectorState: FeeSelectorState.Content, onShow: (String) -> Unit): Boolean {
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return false
val highValue = multipleFees.priority.amount.value ?: return false
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return false
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
val diff = (customValue / highValue).toBigInteger()
val isShow = feeSelectorState.selectedFee == FeeType.Custom && diff > SendNotificationFactory.FEE_MAX_DIFF
if (isShow) onShow(diff.toString())
val diff = customValue / highValue
val isShow = feeSelectorState.selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF
if (isShow) onShow(diff.parseBigDecimal(ZERO_DECIMALS, RoundingMode.HALF_UP))
return isShow
}
}
private val FEE_MAX_DIFF = BigDecimal("5")
private const val ZERO_DECIMALS = 0

View file

@ -15,6 +15,8 @@ internal sealed class FeeSelectorState {
val customValues: ImmutableList<SendTextField.CustomFee> = persistentListOf(),
) : FeeSelectorState()
data object Loading : FeeSelectorState()
data object Error : FeeSelectorState()
}

View file

@ -45,8 +45,12 @@ internal class FeeStateFactory(
val state = currentStateProvider()
val feeState = state.feeState ?: return state
return state.copy(
amountState = state.amountState?.copy(isFeeLoading = true),
feeState = feeState.copy(
feeSelectorState = if (feeState.feeSelectorState is FeeSelectorState.Content) {
feeState.feeSelectorState
} else {
FeeSelectorState.Loading
},
notifications = persistentListOf(),
isPrimaryButtonEnabled = false,
),
@ -73,7 +77,6 @@ internal class FeeStateFactory(
val fee = feeConverter.convert(updatedFeeSelectorState)
return state.copy(
amountState = state.amountState?.copy(isFeeLoading = false),
feeState = feeState.copy(
feeSelectorState = updatedFeeSelectorState,
fee = fee,
@ -85,7 +88,6 @@ internal class FeeStateFactory(
fun onFeeOnErrorState(): SendUiState {
val state = currentStateProvider()
return state.copy(
amountState = state.amountState?.copy(isFeeLoading = false),
feeState = state.feeState?.copy(
feeSelectorState = FeeSelectorState.Error,
),

View file

@ -14,7 +14,7 @@ internal class SendFeeStateConverter(
override fun convert(value: Unit): SendStates.FeeState {
return SendStates.FeeState(
feeSelectorState = FeeSelectorState.Error,
feeSelectorState = FeeSelectorState.Loading,
fee = null,
notifications = persistentListOf(),
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,

View file

@ -38,7 +38,6 @@ internal class SendAmountFieldChangeConverter(
return state.copy(
amountState = amountState.copy(
isPrimaryButtonEnabled = !isExceedBalance && !isZero,
subtractedFee = null,
amountTextField = amountTextField.copy(
value = cryptoValue,
fiatValue = fiatValue,

View file

@ -34,7 +34,6 @@ internal class SendAmountFieldMaxAmountConverter(
return state.copy(
amountState = amountState.copy(
isPrimaryButtonEnabled = true,
subtractedFee = null,
amountTextField = amountTextField.copy(
value = cryptoValue,
fiatValue = fiatValue,

View file

@ -22,10 +22,7 @@ internal object AmountStatePreviewData {
walletBalance = stringReference("123.123"),
tokenIconState = TokenIconState.Loading,
segmentedButtonConfig = persistentListOf(),
notifications = persistentListOf(),
isFeeLoading = false,
appCurrencyCode = "usd",
subtractedFee = null,
amountTextField = SendTextField.AmountField(
value = "123.123123123123123123",
onValueChange = {},
@ -49,6 +46,7 @@ internal object AmountStatePreviewData {
isError = false,
error = TextReference.EMPTY,
),
isSegmentedButtonsEnabled = true,
)
val fiatAmountState = amountState.copy(

View file

@ -34,14 +34,12 @@ internal object SendClickIntentsStub : SendClickIntents {
override fun onRecipientMemoValueChange(value: String) {}
override fun feeReload(isToNextState: Boolean) {}
override fun feeReload() {}
override fun onFeeSelectorClick(feeType: FeeType) {}
override fun onCustomFeeValueChange(index: Int, value: String) {}
override fun onSubtractSelect() {}
override fun onReadMoreClick() {}
override fun onSendClick() {}

View file

@ -4,7 +4,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toDateFormat
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -81,7 +81,7 @@ internal class SendRecipientHistoryListConverter(
}
private fun TxHistoryItem.extractTimestamp(): TextReference {
val date = timestampInMillis.toDateFormat(
val date = timestampInMillis.toDateFormatWithTodayYesterday(
formatter = DateTimeFormatters.dateDDMMYYYY,
)
val time = timestampInMillis.toTimeFormat()

View file

@ -18,22 +18,25 @@ internal class SendRecipientWalletListConverter :
}
}
private fun List<AvailableWallet?>.filterWallets() = this.filterNotNull()
.groupBy { item -> item.name }
.values.map {
it.mapIndexed { index, item ->
val name = if (it.size > 1) {
"${item.name} ${index.inc()}"
} else {
item.name
private fun List<AvailableWallet?>.filterWallets(): PersistentList<SendRecipientListContent> {
var walletsCounter = 0
return this.filterNotNull()
.groupBy { item -> item.name }
.values.map {
it.mapIndexed { index, item ->
val name = if (it.size > 1) {
"${item.name} ${index.inc()}"
} else {
item.name
}
SendRecipientListContent(
id = "${WALLET_KEY_TAG}${walletsCounter++}",
title = TextReference.Str(item.address),
subtitle = TextReference.Str(name),
)
}
SendRecipientListContent(
id = "${WALLET_KEY_TAG}$index",
title = TextReference.Str(item.address),
subtitle = TextReference.Str(name),
)
}
}
.flatten()
.toPersistentList()
.flatten()
.toPersistentList()
}
}

View file

@ -78,7 +78,6 @@ private fun SendNavigationButton(
val isFromConfirmation = currentState.isFromConfirmation
val isCorrectScreen = currentState.type == SendUiStateType.Amount || currentState.type == SendUiStateType.Fee
val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess && !isSending
val showProgress = uiState.amountState?.isFeeLoading == true
val (buttonTextId, buttonClick) = getButtonData(
currentState = currentState,
@ -120,7 +119,7 @@ private fun SendNavigationButton(
if (isSendingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
buttonClick()
},
showProgress = showProgress,
showProgress = false,
modifier = Modifier.fillMaxWidth(),
colors = TangemButtonsDefaults.primaryButtonColors,
)

View file

@ -28,7 +28,7 @@ private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey"
internal fun LazyListScope.buttons(
segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
clickIntents: SendClickIntents,
isMaxButtonEnabled: Boolean,
isSegmentedButtonsEnabled: Boolean,
) {
item(
key = AMOUNT_BUTTONS_KEY,
@ -48,15 +48,18 @@ internal fun LazyListScope.buttons(
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clickIntents.onCurrencyChangeClick(it.isFiat)
},
isEnabled = isSegmentedButtonsEnabled,
) {
SendAmountCurrencyButton(it)
SendAmountCurrencyButton(
button = it,
isSegmentedButtonsEnabled = isSegmentedButtonsEnabled,
)
}
} else {
SpacerWMax()
}
SecondaryButton(
text = stringResource(R.string.send_max_amount),
enabled = isMaxButtonEnabled,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clickIntents.onMaxValueClick()
@ -72,7 +75,7 @@ internal fun LazyListScope.buttons(
}
@Composable
private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) {
private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig, isSegmentedButtonsEnabled: Boolean) {
Row(
modifier = Modifier
.fillMaxSize()
@ -86,17 +89,16 @@ private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) {
FiatIcon(
url = button.iconUrl,
size = TangemTheme.dimens.size18,
isGrayscale = !isSegmentedButtonsEnabled,
modifier = Modifier.size(TangemTheme.dimens.size18),
)
} else {
button.iconState?.let {
TokenIcon(
state = it,
shouldDisplayNetwork = false,
modifier = Modifier
.size(TangemTheme.dimens.size18),
)
}
} else if (button.iconState != null) {
TokenIcon(
state = button.iconState,
shouldDisplayNetwork = false,
modifier = Modifier
.size(TangemTheme.dimens.size18),
)
}
Text(
text = button.title.resolveReference(),

View file

@ -22,11 +22,10 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.rememberDecimalFormat
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import kotlinx.coroutines.delay
import kotlinx.coroutines.job
@Composable
internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolean, appCurrencyCode: String) {
internal fun AmountField(sendField: SendTextField.AmountField, appCurrencyCode: String) {
val decimalFormat = rememberDecimalFormat()
val isFiatValue = sendField.isFiatValue
val currencyCode = if (isFiatValue) appCurrencyCode else null
@ -36,15 +35,6 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea
sendField.cryptoAmount to sendField.value
}
val requester = remember { FocusRequester() }
var isEnabledProxy by remember { mutableStateOf(isEnabled) }
// Fix animation from amount screen to summary screen ([REDACTED_TASK_KEY])
LaunchedEffect(key1 = isEnabled) {
if (isEnabled) {
delay(timeMillis = 700)
}
isEnabledProxy = isEnabled
}
AmountTextField(
value = primaryValue,
@ -62,7 +52,6 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
),
isEnabled = isEnabledProxy,
isAutoResize = true,
modifier = Modifier
.focusRequester(requester)

View file

@ -62,7 +62,6 @@ internal fun LazyListScope.amountField(
)
AmountField(
sendField = amountState.amountTextField,
isEnabled = !amountState.isFeeLoading,
appCurrencyCode = amountState.appCurrencyCode,
)
}

View file

@ -12,7 +12,6 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData
import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub
import com.tangem.features.send.impl.presentation.ui.common.notifications
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
@Composable
@ -35,9 +34,8 @@ internal fun SendAmountContent(
buttons(
segmentedButtonConfig = amountState.segmentedButtonConfig,
clickIntents = clickIntents,
isMaxButtonEnabled = !amountState.isFeeLoading,
isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled,
)
notifications(amountState.notifications)
}
}

View file

@ -1,21 +1,22 @@
package com.tangem.features.send.impl.presentation.ui.send
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.rows.SelectorRowItem
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
@ -26,20 +27,12 @@ import com.tangem.features.send.impl.presentation.utils.getFiatReference
@Composable
internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) {
val fee = feeState.fee ?: return
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
val (title, icon) = when (feeSelectorState.selectedFee) {
FeeType.Slow -> R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24
FeeType.Market -> R.string.common_fee_selector_option_market to R.drawable.ic_bird_24
FeeType.Fast -> R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24
FeeType.Custom -> R.string.common_fee_selector_option_custom to R.drawable.ic_edit_24
}
Column(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(enabled = !isSuccess) { onClick() }
.clickable(enabled = !isSuccess && feeState.fee != null) { onClick() }
.padding(TangemTheme.dimens.spacing12),
) {
Text(
@ -47,17 +40,71 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
)
SelectorRowItem(
titleRes = title,
iconRes = icon,
preDot = getCryptoReference(fee.amount, feeState.isFeeApproximate),
postDot = getFiatReference(fee.amount, feeState.rate, feeState.appCurrency),
ellipsizeOffset = fee.amount.currencySymbol.length,
isSelected = true,
showDivider = false,
paddingValues = PaddingValues(),
Box(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
) {
val feeSelectorState = feeState.feeSelectorState
val feeAmount = feeState.fee?.amount
val (title, icon) = if (feeSelectorState is FeeSelectorState.Content) {
when (feeSelectorState.selectedFee) {
FeeType.Slow -> R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24
FeeType.Market -> R.string.common_fee_selector_option_market to R.drawable.ic_bird_24
FeeType.Fast -> R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24
FeeType.Custom -> R.string.common_fee_selector_option_custom to R.drawable.ic_edit_24
}
} else {
R.string.common_fee_selector_option_market to R.drawable.ic_bird_24
}
SelectorRowItem(
titleRes = title,
iconRes = icon,
preDot = getCryptoReference(feeAmount, feeState.isFeeApproximate),
postDot = feeAmount?.let { getFiatReference(it, feeState.rate, feeState.appCurrency) },
ellipsizeOffset = feeAmount?.currencySymbol?.length,
isSelected = true,
showDivider = false,
paddingValues = PaddingValues(),
)
FeeLoading(feeSelectorState)
FeeError(feeSelectorState)
}
}
}
@Composable
private fun BoxScope.FeeLoading(feeSelectorState: FeeSelectorState) {
AnimatedContent(
targetState = feeSelectorState,
label = "Fee Loading State Change",
modifier = Modifier.align(Alignment.CenterEnd),
) {
if (it == FeeSelectorState.Loading) {
RectangleShimmer(
radius = TangemTheme.dimens.radius3,
modifier = Modifier.size(
height = TangemTheme.dimens.size12,
width = TangemTheme.dimens.size90,
),
)
}
}
}
@Composable
private fun BoxScope.FeeError(feeSelectorState: FeeSelectorState) {
AnimatedContent(
targetState = feeSelectorState,
label = "Fee Error State Change",
modifier = Modifier.align(Alignment.CenterEnd),
) {
if (it == FeeSelectorState.Error) {
Text(
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body2,
)
}
}
}

View file

@ -4,6 +4,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -40,6 +41,7 @@ internal fun RecipientBlock(
.padding(TangemTheme.dimens.spacing12),
) {
AddressBlock(recipientState.addressTextField)
MemoBlock(recipientState.memoTextField)
}
}
@ -70,6 +72,28 @@ private fun AddressBlock(address: SendTextField.RecipientAddress) {
}
}
@Composable
private fun MemoBlock(memo: SendTextField.RecipientMemo?) {
val showMemo = memo != null && memo.value.isNotBlank()
if (showMemo) {
HorizontalDivider(
color = TangemTheme.colors.icon.inactive,
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
)
Text(
text = memo?.label?.resolveReference().orEmpty(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
)
Text(
text = memo?.value.orEmpty(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
}
// region Preview
@Preview
@Composable

View file

@ -39,14 +39,12 @@ internal interface SendClickIntents {
// endregion
// region Fee
fun feeReload(isToNextState: Boolean = false)
fun feeReload()
fun onFeeSelectorClick(feeType: FeeType)
fun onCustomFeeValueChange(index: Int, value: String)
fun onSubtractSelect()
fun onReadMoreClick()
// endregion

View file

@ -24,6 +24,7 @@ import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.utils.convertToAmount
import com.tangem.domain.transaction.error.GetFeeError
@ -156,10 +157,10 @@ internal class SendViewModel @Inject constructor(
private val sendNotificationFactory = SendNotificationFactory(
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
feePaidCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
currentStateProvider = Provider { uiState },
userWalletProvider = Provider { userWallet },
stateRouterProvider = Provider { stateRouter },
isSubtractAvailableProvider = Provider { isAmountSubtractAvailable },
currencyChecksRepository = currencyChecksRepository,
clickIntents = this,
analyticsEventHandler = analyticsEventHandler,
@ -191,7 +192,6 @@ internal class SendViewModel @Inject constructor(
private var qrScannerJobHolder = JobHolder()
private var sendIdleTimer = 0L
private var feeIdleTimer = 0L
init {
subscribeOnCurrencyStatusUpdates()
@ -359,13 +359,14 @@ internal class SendViewModel @Inject constructor(
stateRouter.showSend()
}
transactionId != null && amount != null && destinationAddress != null -> {
loadFee()
uiState = stateFactory.getReadyState(amount, destinationAddress, memo)
stateRouter.showFee()
stateRouter.showSend()
updateNotifications()
}
else -> {
getWalletsAndRecent()
uiState = stateFactory.getReadyState()
getWalletsAndRecent()
stateRouter.showRecipient()
updateNotifications()
}
@ -406,7 +407,8 @@ internal class SendViewModel @Inject constructor(
return if (!isMultiCurrency) {
val status = getCryptoCurrencyStatusSyncUseCase(walletId).getOrNull()
val address = status?.value?.networkAddress.takeIf {
status?.currency?.network?.id == cryptoCurrency.network.id
status?.currency?.network?.id == cryptoCurrency.network.id &&
status.currency.network.derivationPath !is Network.DerivationPath.Custom
}
address?.let {
AvailableWallet(
@ -417,7 +419,8 @@ internal class SendViewModel @Inject constructor(
} else {
val statuses = getCryptoCurrencyStatusesSyncUseCase(walletId).getOrNull()
val walletCurrency = statuses?.firstOrNull {
it.currency.network.id == cryptoCurrency.network.id
it.currency.network.id == cryptoCurrency.network.id &&
it.currency.network.derivationPath !is Network.DerivationPath.Custom
}
val address = walletCurrency?.value?.networkAddress
address?.let {
@ -484,12 +487,7 @@ internal class SendViewModel @Inject constructor(
if (onFeeNext()) return
}
SendUiStateType.Amount -> {
if (uiState.feeState?.feeSelectorState is FeeSelectorState.Content) {
if (onFeeCoverageAlert()) return
} else {
loadFee(isToNextState = true)
return
}
loadFee()
}
else -> Unit
}
@ -515,15 +513,15 @@ internal class SendViewModel @Inject constructor(
innerRouter.openTokenDetails(userWalletId, currency)
private fun onFeeNext(): Boolean {
if (onFeeCoverageAlert()) return true
if (checkIfFeeTooLow(uiState)) {
uiState = eventStateFactory.getFeeTooLowAlert(
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
)
return true
}
val feeSelectorState = uiState.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false
return checkIfFeeTooHigh(
state = uiState,
feeSelectorState = feeSelectorState,
onShow = { diff ->
uiState = eventStateFactory.getFeeTooHighAlert(
diff = diff,
@ -533,19 +531,6 @@ internal class SendViewModel @Inject constructor(
)
}
private fun onFeeCoverageAlert(): Boolean {
val isFeeCoverage = checkFeeCoverage(uiState, cryptoCurrencyStatus)
return if (isAmountSubtractAvailable && isFeeCoverage) {
uiState = eventStateFactory.getFeeCoverageAlert(
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
)
true
} else {
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
false
}
}
private fun cancelFeeRequest() {
viewModelScope.launch(dispatchers.main) {
feeJobHolder.cancel()
@ -647,7 +632,7 @@ internal class SendViewModel @Inject constructor(
// endregion
// region fee
override fun feeReload(isToNextState: Boolean) = loadFee(isToNextState = isToNextState)
override fun feeReload() = loadFee()
override fun onFeeSelectorClick(feeType: FeeType) {
uiState = feeStateFactory.onFeeSelectedState(feeType)
@ -662,12 +647,6 @@ internal class SendViewModel @Inject constructor(
updateFeeNotifications()
}
override fun onSubtractSelect() {
uiState = stateFactory.onSubtractSelect(isAmountSubtractAvailable)
stateRouter.showSend()
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(true))
}
override fun onReadMoreClick() {
val locale = if (Locale.getDefault().language == RU_LOCALE) RU_LOCALE else EN_LOCALE
val url = buildString {
@ -678,10 +657,7 @@ internal class SendViewModel @Inject constructor(
innerRouter.openUrl(url)
}
private fun loadFee(isToNextState: Boolean = false) {
// debouncing fee request
if (SystemClock.elapsedRealtime() - feeIdleTimer < FEE_UPDATE_DELAY) return
private fun loadFee() {
viewModelScope.launch(dispatchers.main) {
val isShowStatus = uiState.feeState?.fee == null
if (isShowStatus) {
@ -689,35 +665,25 @@ internal class SendViewModel @Inject constructor(
}
val result = callFeeUseCase()?.fold(
ifRight = {
feeIdleTimer = SystemClock.elapsedRealtime()
uiState = feeStateFactory.onFeeOnLoadedState(it)
if (isToNextState && !onFeeCoverageAlert()) {
stateRouter.showSend()
}
},
ifLeft = {
onFeeLoadFailed(isShowStatus, isToNextState)
onFeeLoadFailed(isShowStatus)
},
)
if (result == null) {
onFeeLoadFailed(isShowStatus, isToNextState)
onFeeLoadFailed(isShowStatus)
}
updateNotifications()
updateFeeNotifications()
}.saveIn(feeJobHolder)
.invokeOnCompletion {
uiState = amountStateFactory.getOnAmountFeeLoadingCancel()
// todo
}
}
private fun onFeeLoadFailed(isShowStatus: Boolean, isToNextState: Boolean) {
when {
isToNextState -> {
uiState = eventStateFactory.getFeeUnreachableErrorState {
uiState = eventStateFactory.onConsumeEventState()
}
}
isShowStatus -> uiState = feeStateFactory.onFeeOnErrorState()
}
private fun onFeeLoadFailed(isShowStatus: Boolean) {
if (isShowStatus) uiState = feeStateFactory.onFeeOnErrorState()
}
private suspend fun checkIfSubtractAvailable() {
@ -735,7 +701,7 @@ internal class SendViewModel @Inject constructor(
return getFeeUseCase.invoke(
amount = amount,
destination = recipientState.addressTextField.value,
userWalletId = userWalletId,
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
)
}
@ -751,7 +717,6 @@ internal class SendViewModel @Inject constructor(
verifyAndSendTransaction()
} else {
onCheckFeeUpdate()
feeIdleTimer = SystemClock.elapsedRealtime()
sendIdleTimer = SystemClock.elapsedRealtime()
}
}
@ -792,10 +757,11 @@ internal class SendViewModel @Inject constructor(
override fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class<out SendNotification>) {
uiState = amountStateFactory.getOnAmountValueChange(reducedAmount.parseBigDecimal(cryptoCurrency.decimals))
uiState = sendNotificationFactory.dismissNotificationState(clazz)
feeReload()
}
override fun onNotificationCancel(clazz: Class<out SendNotification>) {
uiState = sendNotificationFactory.dismissNotificationState(clazz)
uiState = sendNotificationFactory.dismissNotificationState(clazz = clazz, isIgnored = true)
}
private fun verifyAndSendTransaction() {
@ -804,10 +770,18 @@ internal class SendViewModel @Inject constructor(
val fee = feeState.fee ?: return
val memo = uiState.recipientState?.memoTextField?.value
val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return
val feeValue = fee.amount.value ?: return
val receivingAmount = checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
)
viewModelScope.launch(dispatchers.main) {
createTransactionUseCase(
amount = amountValue.convertToAmount(cryptoCurrency),
amount = receivingAmount.convertToAmount(cryptoCurrency),
fee = fee,
memo = memo,
destination = recipient,
@ -920,7 +894,6 @@ internal class SendViewModel @Inject constructor(
private companion object {
const val CHECK_FEE_UPDATE_DELAY = 60_000L
const val FEE_UPDATE_DELAY = 10_000L
const val BALANCE_UPDATE_DELAY = 10_000L
const val RU_LOCALE = "ru"