Updated on 2026-08-14
This commit is contained in:
parent
385cc79a32
commit
a3f2c421bf
69 changed files with 749 additions and 542 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,6 @@ internal class SendConfirmStateConverter(
|
|||
isPrimaryButtonEnabled = true,
|
||||
isSending = false,
|
||||
isSuccess = false,
|
||||
isSubtract = false,
|
||||
transactionDate = 0L,
|
||||
txUrl = "",
|
||||
ignoreAmountReduce = false,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -15,6 +15,8 @@ internal sealed class FeeSelectorState {
|
|||
val customValues: ImmutableList<SendTextField.CustomFee> = persistentListOf(),
|
||||
) : FeeSelectorState()
|
||||
|
||||
data object Loading : FeeSelectorState()
|
||||
|
||||
data object Error : FeeSelectorState()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ internal class SendAmountFieldMaxAmountConverter(
|
|||
return state.copy(
|
||||
amountState = amountState.copy(
|
||||
isPrimaryButtonEnabled = true,
|
||||
subtractedFee = null,
|
||||
amountTextField = amountTextField.copy(
|
||||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ internal fun LazyListScope.amountField(
|
|||
)
|
||||
AmountField(
|
||||
sendField = amountState.amountTextField,
|
||||
isEnabled = !amountState.isFeeLoading,
|
||||
appCurrencyCode = amountState.appCurrencyCode,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue