Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-02 10:50:20 +01:00
commit 62e5621379
44 changed files with 509 additions and 235 deletions

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>

View file

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>

View file

@ -1,11 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
<domain-config>
<domain includeSubdomains="true">api.tangem.com</domain>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</domain-config>
</base-config>
</network-security-config>

View file

@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.reactive.asFlow
private const val PING_SERVER = "https://clients3.google.com/generate_204"
private const val PING_INTERVAL = 5_000
internal class RealInternetConnectionManager : NetworkConnectionManager {
@ -24,7 +25,12 @@ internal class RealInternetConnectionManager : NetworkConnectionManager {
private val initialNetworkResult: Boolean by lazy {
ReactiveNetwork
.checkInternetConnectivity()
.checkInternetConnectivity(
InternetObservingSettings.builder()
.host(PING_SERVER)
.port(443)
.build(),
)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.blockingGet()
@ -33,6 +39,8 @@ internal class RealInternetConnectionManager : NetworkConnectionManager {
override val isOnlineFlow: StateFlow<Boolean> = ReactiveNetwork
.observeInternetConnectivity(
InternetObservingSettings.builder()
.host(PING_SERVER)
.port(443)
.interval(PING_INTERVAL)
.build(),
)

View file

@ -63,7 +63,8 @@ fun AppBarWithBackButtonAndIconContent(
)
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier.weight(1f),
modifier = Modifier.weight(1f)
.animateContentSize(),
) {
AnimatedVisibility(
visible = !text.isNullOrBlank(),
@ -89,7 +90,6 @@ fun AppBarWithBackButtonAndIconContent(
color = TangemTheme.colors.text.secondary,
maxLines = 1,
style = TangemTheme.typography.caption2,
modifier = Modifier.animateContentSize(),
)
}
}

View file

@ -60,24 +60,23 @@ sealed class TokenIconState {
* @property background The background color to be used for the icon.
* @property networkBadgeIconResId The drawable resource ID for the network badge.
* @property isGrayscale Specifies whether to show the icon in grayscale.
* @property showCustomBadge Specifies whether to show the custom token badge.
*/
data class CustomTokenIcon(
val tint: Color,
val background: Color,
@DrawableRes override val networkBadgeIconResId: Int,
override val isGrayscale: Boolean,
) : TokenIconState() {
override val showCustomBadge: Boolean = true,
) : TokenIconState()
override val showCustomBadge: Boolean = true
}
object Loading : TokenIconState() {
data object Loading : TokenIconState() {
override val isGrayscale: Boolean = false
override val showCustomBadge: Boolean = false
override val networkBadgeIconResId: Int? = null
}
object Locked : TokenIconState() {
data object Locked : TokenIconState() {
override val isGrayscale: Boolean = false
override val showCustomBadge: Boolean = false
override val networkBadgeIconResId: Int? = null

View file

@ -30,6 +30,7 @@ class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, Token
coin = currency,
isUnreachable = value.value.isError,
forceGrayscale = forceGrayscale,
showCustomBadge = showCustomTokenBadge,
)
is CryptoCurrency.Token -> getIconStateForToken(
token = currency,
@ -50,13 +51,14 @@ class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, Token
private fun getIconStateForCoin(
coin: CryptoCurrency.Coin,
isUnreachable: Boolean,
showCustomBadge: Boolean = true,
forceGrayscale: Boolean = false,
): TokenIconState.CoinIcon {
return TokenIconState.CoinIcon(
url = coin.iconUrl,
fallbackResId = coin.networkIconResId,
isGrayscale = forceGrayscale || coin.network.isTestnet || isUnreachable,
showCustomBadge = coin.isCustom,
showCustomBadge = coin.isCustom && showCustomBadge,
)
}
@ -76,6 +78,7 @@ class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, Token
background = background,
networkBadgeIconResId = token.networkIconResId,
isGrayscale = grayScale,
showCustomBadge = showCustomBadge,
)
} else {
TokenIconState.TokenIcon(

View file

@ -45,14 +45,19 @@ fun SimpleTextField(
) {
val proxyValue by remember(value) { derivedStateOf { value } }
var textFieldValueState by remember {
mutableStateOf(TextFieldValue(text = value))
mutableStateOf(
TextFieldValue(
text = value,
selection = TextRange(value.length, value.length),
),
)
}
val focusRequester = remember { FocusRequester.Default }
val customTextSelectionColors = TextSelectionColors(
handleColor = TangemTheme.colors.text.accent,
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
)
val textFieldValue = textFieldValueState.copy(text = value)
var lastTextValue by remember(proxyValue, isValuePasted) {
textFieldValueState = textFieldValueState.copy(
text = proxyValue,
@ -65,6 +70,13 @@ fun SimpleTextField(
mutableStateOf(proxyValue)
}
val isSelectionChanged by rememberSelectionChanged(textFieldValue, textFieldValueState)
LaunchedEffect(key1 = isSelectionChanged) {
if (isSelectionChanged) {
textFieldValueState = textFieldValue
}
}
// resets paste value cursor trigger
LaunchedEffect(key1 = isValuePasted) {
if (isValuePasted) {
@ -74,7 +86,7 @@ fun SimpleTextField(
CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) {
BasicTextField(
value = textFieldValueState,
value = textFieldValue,
onValueChange = { newTextFieldValueState ->
textFieldValueState = newTextFieldValueState
@ -126,4 +138,14 @@ private fun SimpleTextPlaceholder(
}
textValue()
}
}
@Composable
private fun rememberSelectionChanged(textFieldValue: TextFieldValue, textFieldValueState: TextFieldValue) = remember {
derivedStateOf {
val isSelectionChanged = textFieldValue.selection != textFieldValueState.selection ||
textFieldValue.composition != textFieldValueState.composition
val isTextNotChanged = textFieldValue.text == textFieldValueState.text
isSelectionChanged && isTextNotChanged
}
}

View file

@ -73,7 +73,7 @@ fun SelectorRowItem(
color = TangemTheme.colors.text.primary1,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing8),
)
if (preDot != null && postDot != null) {
if (preDot != null) {
SelectorValueContent(
preDot = preDot,
postDot = postDot,
@ -97,7 +97,7 @@ fun SelectorRowItem(
@Composable
private fun RowScope.SelectorValueContent(
preDot: TextReference,
postDot: TextReference,
postDot: TextReference?,
ellipsizeOffset: Int? = null,
) {
val ellipsis = if (ellipsizeOffset == null) {
@ -115,18 +115,20 @@ private fun RowScope.SelectorValueContent(
.weight(1f)
.padding(start = TangemTheme.dimens.spacing4),
)
Text(
text = "",
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing4),
)
Text(
text = postDot.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
if (postDot != null) {
Text(
text = "",
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing4),
)
Text(
text = postDot.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Preview

View file

@ -71,7 +71,10 @@ internal class DefaultTransactionRepository(
}
Blockchain.Binance -> BinanceTransactionExtras(memo)
Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) }
Blockchain.Cosmos -> CosmosTransactionExtras(memo)
Blockchain.Cosmos,
Blockchain.TerraV1,
Blockchain.TerraV2,
-> CosmosTransactionExtras(memo)
Blockchain.TON -> TonTransactionExtras(memo)
Blockchain.Hedera -> HederaTransactionBuilder.HederaTransactionExtras(memo)
Blockchain.Algorand -> AlgorandTransactionExtras(memo)

View file

@ -264,10 +264,18 @@ internal class CurrenciesStatusesOperations(
var quotesRetrievingFailed = false
val networksStatuses = maybeNetworkStatuses?.bind()?.toNonEmptySetOrNull()
val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) {
quotesRetrievingFailed = true
null
}
val quotes: Set<Quote>? = maybeQuotes?.fold(
ifLeft = {
quotesRetrievingFailed = true
null
},
ifRight = {
it.ifEmpty {
quotesRetrievingFailed = true
null
}
},
)
currencies.map { currency ->
val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }

View file

@ -16,6 +16,20 @@ internal sealed class SendAnalyticEvents(
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Token / Send", event = event, params = params) {
/** Close button clicked */
data class CloseButtonClicked(
val source: SendScreenSource,
val isFromSummary: Boolean,
val isValid: Boolean,
) : SendAnalyticEvents(
event = "Button - Close",
params = mapOf(
SOURCE to source.name,
"FromSummary" to if (isFromSummary) "Yes" else "No",
"isValid" to if (isValid) "Yes" else "No",
),
)
// region Address
/** Recipient address screen opened */
data object AddressScreenOpened : SendAnalyticEvents(event = "Address Screen Opened")
@ -119,6 +133,7 @@ internal enum class SendScreenSource {
Address,
Amount,
Fee,
Confirm,
}
internal enum class EnterAddressSource {

View file

@ -5,15 +5,17 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.features.send.impl.presentation.analytics.SelectedCurrencyType
import com.tangem.features.send.impl.presentation.analytics.SelectedFeeType
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
import com.tangem.features.send.impl.presentation.state.*
import com.tangem.features.send.impl.presentation.analytics.SendScreenSource
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.utils.Provider
internal class SendOnNextScreenAnalyticSender(
internal class SendScreenAnalyticSender(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val analyticsEventHandler: AnalyticsEventHandler,
) {
fun send(prevScreen: SendUiStateType, state: SendUiState) {
@ -45,6 +47,32 @@ internal class SendOnNextScreenAnalyticSender(
}
}
fun sendOnClose() {
val routerState = stateRouterProvider().currentState.value
val state = currentStateProvider()
val (source, isValid) = when (routerState.type) {
SendUiStateType.Recipient,
SendUiStateType.EditRecipient,
-> SendScreenSource.Address to (state.editRecipientState?.isPrimaryButtonEnabled ?: false)
SendUiStateType.Amount,
SendUiStateType.EditAmount,
-> SendScreenSource.Amount to (state.editAmountState?.isPrimaryButtonEnabled ?: false)
SendUiStateType.Fee,
SendUiStateType.EditFee,
-> SendScreenSource.Fee to (state.editFeeState?.isPrimaryButtonEnabled ?: false)
else -> SendScreenSource.Confirm to true
}
analyticsEventHandler.send(
SendAnalyticEvents.CloseButtonClicked(
source = source,
isFromSummary = routerState.isFromConfirmation,
isValid = isValid,
),
)
}
private fun sendSelectedFeeAnalytics(feeSelectorState: FeeSelectorState.Content) {
val type = when (feeSelectorState.fees) {
is TransactionFee.Single -> SelectedFeeType.Fixed

View file

@ -18,6 +18,7 @@ import com.tangem.features.send.impl.presentation.state.common.SendSyncEditConve
import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
import com.tangem.features.send.impl.presentation.state.fee.checkFeeCoverage
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientHistoryListConverter
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter
@ -28,8 +29,9 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import timber.log.Timber
import java.math.BigDecimal
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
internal class SendStateFactory(
private val clickIntents: SendClickIntents,
private val stateRouterProvider: Provider<StateRouter>,
@ -46,6 +48,7 @@ internal class SendStateFactory(
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
SendAmountFieldConverter(
clickIntents = clickIntents,
stateRouterProvider = stateRouterProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
appCurrencyProvider = appCurrencyProvider,
)
@ -94,6 +97,7 @@ internal class SendStateFactory(
isEditingDisabled = false,
isBalanceHidden = false,
cryptoCurrencyName = "",
isSubtracted = false,
)
fun getReadyState(): SendUiState {
@ -279,12 +283,33 @@ internal class SendStateFactory(
//endregion
//region send
fun getIsAmountSubtractedState(isAmountSubtractAvailable: Boolean): SendUiState {
val state = currentStateProvider()
val balance = cryptoCurrencyStatusProvider().value.amount ?: return state
val amountState = state.getAmountState(stateRouterProvider().isEditState) ?: return state
val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return state
val amountValue = amountState.amountTextField.cryptoAmount.value ?: return state
val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO
return state.copy(
isSubtracted = checkFeeCoverage(
isSubtractAvailable = isAmountSubtractAvailable,
balance = balance,
amountValue = amountValue,
feeValue = feeValue,
),
)
}
fun getSendingStateUpdate(isSending: Boolean): SendUiState {
val state = currentStateProvider()
return state.copy(
sendState = state.sendState?.copy(
isSending = isSending,
isPrimaryButtonEnabled = isPrimaryButtonEnabled(state, state.sendState.notifications),
isPrimaryButtonEnabled = isPrimaryButtonEnabled(
state = state,
isSending = isSending,
notifications = state.sendState.notifications,
),
),
)
}
@ -308,7 +333,11 @@ internal class SendStateFactory(
val sendState = state.sendState ?: return state
return state.copy(
sendState = sendState.copy(
isPrimaryButtonEnabled = isPrimaryButtonEnabled(state, notifications),
isPrimaryButtonEnabled = isPrimaryButtonEnabled(
state = state,
isSending = sendState.isSending,
notifications = notifications,
),
notifications = notifications,
showTapHelp = sendState.showTapHelp && notifications.isEmpty(),
),
@ -323,11 +352,14 @@ internal class SendStateFactory(
)
}
private fun isPrimaryButtonEnabled(state: SendUiState, notifications: ImmutableList<SendNotification>): Boolean {
val sendState = state.sendState ?: return false
private fun isPrimaryButtonEnabled(
state: SendUiState,
isSending: Boolean,
notifications: ImmutableList<SendNotification>,
): Boolean {
val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return false
val hasErrorNotifications = notifications.any { it is SendNotification.Error }
return !hasErrorNotifications && !sendState.isSending && feeState.feeSelectorState is FeeSelectorState.Content
return !hasErrorNotifications && !isSending && feeState.feeSelectorState is FeeSelectorState.Content
}
//endregion
}

View file

@ -32,6 +32,7 @@ internal data class SendUiState(
val editRecipientState: SendStates.RecipientState? = null,
val editFeeState: SendStates.FeeState? = null,
val isBalanceHidden: Boolean,
val isSubtracted: Boolean,
val event: StateEvent<SendEvent>,
) {
@ -97,6 +98,7 @@ internal sealed class SendStates {
val walletBalance: TextReference,
val tokenIconState: TokenIconState,
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
val selectedButton: Int,
val isSegmentedButtonsEnabled: Boolean,
val amountTextField: SendTextField.AmountField,
val appCurrencyCode: String,
@ -139,6 +141,7 @@ internal sealed class SendStates {
val transactionDate: Long,
val txUrl: String,
val ignoreAmountReduce: Boolean,
val reduceAmountBy: BigDecimal?,
val isFromConfirmation: Boolean,
val showTapHelp: Boolean,
val notifications: ImmutableList<SendNotification>,

View file

@ -34,7 +34,7 @@ internal class StateRouter(
when {
isSuccess -> popBackStack()
isEditingDisabled -> when (type) {
SendUiStateType.Send -> showFee()
SendUiStateType.EditFee -> showSend()
else -> popBackStack()
}
else -> when (type) {

View file

@ -6,6 +6,7 @@ import com.tangem.features.send.impl.presentation.state.StateRouter
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 java.math.BigDecimal
/**
* Factory to produce amount state for [SendUiState]
@ -44,9 +45,18 @@ internal class AmountStateFactory(
currentStateProvider = currentStateProvider,
)
}
private val amountReducedConverter by lazy {
SendAmountReducedConverter(
stateRouterProvider = stateRouterProvider,
currentStateProvider = currentStateProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value)
fun getOnAmountReducedState(reduceAmountBy: BigDecimal) = amountReducedConverter.convert(reduceAmountBy)
fun getOnMaxAmountClick(): SendUiState {
return amountFieldMaxAmountConverter.convert(Unit)
}

View file

@ -0,0 +1,59 @@
package com.tangem.features.send.impl.presentation.state.amount
import androidx.compose.ui.text.input.ImeAction
import com.tangem.common.extensions.isZero
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.fields.SendTextField
import java.math.BigDecimal
import java.math.RoundingMode
internal fun String.getCryptoValue(fiatRate: BigDecimal?, isFiatValue: Boolean, decimals: Int): String {
return if (isFiatValue && fiatRate != null) {
parseToBigDecimal(decimals).divide(fiatRate, decimals, RoundingMode.DOWN)
.parseBigDecimal(decimals)
} else {
this
}
}
internal fun String.getFiatValue(
fiatRate: BigDecimal?,
isFiatValue: Boolean,
decimals: Int,
): Pair<String, BigDecimal?> {
return if (fiatRate != null) {
val fiatValue = if (!isFiatValue) {
parseToBigDecimal(decimals).multiply(fiatRate).parseBigDecimal(decimals)
} else {
this
}
val decimalFiatValue = fiatValue.parseToBigDecimal(decimals)
fiatValue to decimalFiatValue
} else {
"" to null
}
}
internal fun String.checkExceedBalance(
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountTextField: SendTextField.AmountField,
): Boolean {
val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO
val fiatDecimal = parseToBigDecimal(amountTextField.fiatAmount.decimals)
val cryptoDecimal = parseToBigDecimal(amountTextField.cryptoAmount.decimals)
return if (amountTextField.isFiatValue) {
fiatDecimal > currencyFiatAmount
} else {
cryptoDecimal > currencyCryptoAmount
}
}
internal fun getKeyboardAction(isExceedBalance: Boolean, decimalCryptoValue: BigDecimal) =
if (!isExceedBalance && !decimalCryptoValue.isZero()) {
ImeAction.Done
} else {
ImeAction.None
}

View file

@ -1,5 +1,8 @@
package com.tangem.features.send.impl.presentation.state.amount
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
@ -20,6 +23,7 @@ internal class SendAmountCurrencyConverter(
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val isValidFiatRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero()
val isDoneActionEnabled = amountState.isPrimaryButtonEnabled
return if (amountTextField.isFiatValue == value && !isValidFiatRate) {
state
} else {
@ -29,7 +33,12 @@ internal class SendAmountCurrencyConverter(
amountTextField = amountTextField.copy(
isFiatValue = value,
isValuePasted = true,
keyboardOptions = KeyboardOptions(
imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None,
keyboardType = KeyboardType.Number,
),
),
selectedButton = amountState.segmentedButtonConfig.indexOfFirst { it.isFiat == value },
),
)
}

View file

@ -0,0 +1,62 @@
package com.tangem.features.send.impl.presentation.state.amount
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.extensions.isZero
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.features.send.impl.presentation.state.StateRouter
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
import java.math.BigDecimal
internal class SendAmountReducedConverter(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<BigDecimal, SendUiState> {
override fun convert(value: BigDecimal): SendUiState {
val state = currentStateProvider()
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val isEditState = stateRouterProvider().isEditState
val amountState = state.getAmountState(isEditState) ?: return state
val amountTextField = amountState.amountTextField
val cryptoDecimals = amountTextField.cryptoAmount.decimals
val fiatDecimals = amountTextField.fiatAmount.decimals
val amountValue = amountState.amountTextField.cryptoAmount.value ?: return state
val decimalCryptoValue = amountValue.minus(value)
val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals)
val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue(
fiatRate = cryptoCurrencyStatus.value.fiatRate,
isFiatValue = false,
decimals = fiatDecimals,
)
val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue
val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField)
val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else decimalCryptoValue.isZero()
return state.copyWrapped(
isEditState = isEditState,
sendState = state.sendState?.copy(
reduceAmountBy = value,
),
amountState = amountState.copy(
isPrimaryButtonEnabled = !isExceedBalance && !isZero,
amountTextField = amountTextField.copy(
value = cryptoValue,
fiatValue = fiatValue,
isError = isExceedBalance,
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
keyboardOptions = KeyboardOptions(
imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue),
keyboardType = KeyboardType.Number,
),
),
),
)
}
}

View file

@ -57,6 +57,7 @@ internal class SendAmountStateConverter(
),
),
isSegmentedButtonsEnabled = !noFeeRate,
selectedButton = 0,
)
}
}

View file

@ -16,6 +16,7 @@ internal class SendConfirmStateConverter(
transactionDate = 0L,
txUrl = "",
ignoreAmountReduce = false,
reduceAmountBy = null,
isFromConfirmation = true,
showTapHelp = isTapHelpPreviewEnabledProvider(),
notifications = persistentListOf(),

View file

@ -20,7 +20,6 @@ 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.*
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 kotlinx.collections.immutable.ImmutableList
@ -74,7 +73,6 @@ internal class SendNotificationFactory(
addFeeUnreachableNotification(feeState.feeSelectorState)
addExceedBalanceNotification(feeValue, sendingAmount)
addExceedsBalanceNotification(feeState.fee)
addMinimumAmountErrorNotification(feeValue, sendingAmount)
addDustWarningNotification(feeValue, sendingAmount)
addTransactionLimitErrorNotification(feeValue, sendingAmount)
// warnings
@ -95,6 +93,7 @@ internal class SendNotificationFactory(
return state.copy(
sendState = sendState.copy(
ignoreAmountReduce = isIgnored,
reduceAmountBy = if (isIgnored) null else sendState.reduceAmountBy,
notifications = updatedNotifications.toImmutableList(),
),
)
@ -227,8 +226,7 @@ internal class SendNotificationFactory(
SendNotification.Warning.HighFeeError(
amount = threshold.toPlainString(),
onConfirmClick = {
val reduceTo = sendAmount.minus(threshold)
clickIntents.onAmountReduceClick(reduceTo, SendNotification.Warning.HighFeeError::class.java)
clickIntents.onAmountReduceClick(threshold, SendNotification.Warning.HighFeeError::class.java)
},
onCloseClick = {
clickIntents.onNotificationCancel(SendNotification.Warning.HighFeeError::class.java)
@ -238,21 +236,6 @@ 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,
@ -380,8 +363,4 @@ internal class SendNotificationFactory(
val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO
return receivedAmount < dustValue || isChangeLowerThanDust
}
companion object {
private const val DOGECOIN_MINIMUM = "0.01"
}
}

View file

@ -1,12 +1,10 @@
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.common.extensions.isZero
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.common.extensions.minimalAmount
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.lib.crypto.BlockchainUtils
import java.math.BigDecimal
import java.math.RoundingMode
@ -18,6 +16,7 @@ internal fun checkAndCalculateSubtractedAmount(
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountValue: BigDecimal,
feeValue: BigDecimal,
reduceAmountBy: BigDecimal?,
): BigDecimal {
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
val isFeeCoverage = checkFeeCoverage(
@ -26,12 +25,17 @@ internal fun checkAndCalculateSubtractedAmount(
amountValue = amountValue,
feeValue = feeValue,
)
return calculateSubtractedAmount(
val subtractedAmount = calculateSubtractedAmount(
isFeeCoverage = isFeeCoverage,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
)
return if (reduceAmountBy != null) {
subtractedAmount.minus(reduceAmountBy)
} else {
subtractedAmount
}
}
/**
@ -44,7 +48,7 @@ internal fun checkFeeCoverage(
feeValue: BigDecimal,
): Boolean {
if (!isSubtractAvailable) return false
return balance < amountValue + feeValue && balance > feeValue
return balance < amountValue + feeValue && balance > feeValue && balance >= amountValue
}
/**
@ -58,12 +62,7 @@ internal fun calculateSubtractedAmount(
): 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
minOf(amountValue, balance.minus(feeValue))
} else {
amountValue
}
@ -95,5 +94,12 @@ internal fun checkIfFeeTooHigh(feeSelectorState: FeeSelectorState.Content, onSho
return isShow
}
/**
* Checks if fee exceeds fee paid currency balance
*/
fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean {
return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance
}
private val FEE_MAX_DIFF = BigDecimal("5")
private const val ZERO_DECIMALS = 0

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.state.fee.custom.BitcoinCustomFeeConverter
import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
@ -13,6 +14,7 @@ import com.tangem.utils.converter.Converter
internal class FeeConverter(
private val clickIntents: SendClickIntents,
private val stateRouterProvider: Provider<StateRouter>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
) : Converter<FeeSelectorState.Content, Fee> {
@ -20,6 +22,7 @@ internal class FeeConverter(
private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
EthereumCustomFeeConverter(
clickIntents = clickIntents,
stateRouterProvider = stateRouterProvider,
appCurrencyProvider = appCurrencyProvider,
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)
@ -28,6 +31,7 @@ internal class FeeConverter(
private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
BitcoinCustomFeeConverter(
clickIntents = clickIntents,
stateRouterProvider = stateRouterProvider,
appCurrencyProvider = appCurrencyProvider,
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)

View file

@ -7,7 +7,10 @@ import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.features.send.impl.presentation.state.*
import com.tangem.features.send.impl.presentation.state.SendNotification
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
@ -27,6 +30,7 @@ internal class FeeStateFactory(
private val customFeeFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
SendFeeCustomFieldConverter(
clickIntents = clickIntents,
stateRouterProvider = stateRouterProvider,
appCurrencyProvider = appCurrencyProvider,
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)
@ -35,6 +39,7 @@ internal class FeeStateFactory(
val feeConverter by lazy(LazyThreadSafetyMode.NONE) {
FeeConverter(
clickIntents = clickIntents,
stateRouterProvider = stateRouterProvider,
appCurrencyProvider = appCurrencyProvider,
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)

View file

@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.state.fee
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.state.fee.custom.BitcoinCustomFeeConverter
import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
@ -14,6 +15,7 @@ import kotlinx.collections.immutable.persistentListOf
internal class SendFeeCustomFieldConverter(
private val clickIntents: SendClickIntents,
private val stateRouterProvider: Provider<StateRouter>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
) : Converter<Fee, ImmutableList<SendTextField.CustomFee>> {
@ -21,6 +23,7 @@ internal class SendFeeCustomFieldConverter(
private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
EthereumCustomFeeConverter(
clickIntents = clickIntents,
stateRouterProvider = stateRouterProvider,
appCurrencyProvider = appCurrencyProvider,
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)
@ -29,6 +32,7 @@ internal class SendFeeCustomFieldConverter(
private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
BitcoinCustomFeeConverter(
clickIntents = clickIntents,
stateRouterProvider = stateRouterProvider,
appCurrencyProvider = appCurrencyProvider,
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)

View file

@ -5,17 +5,16 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.extensions.isZero
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.utils.getFiatReference
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.Provider
@ -27,12 +26,14 @@ import java.math.RoundingMode
internal class BitcoinCustomFeeConverter(
private val clickIntents: SendClickIntents,
private val stateRouterProvider: Provider<StateRouter>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
) : CustomFeeConverter<Fee.Bitcoin> {
override fun convert(value: Fee.Bitcoin): ImmutableList<SendTextField.CustomFee> {
val feeValue = value.amount.value
val feeCurrency = feeCryptoCurrencyStatusProvider()?.value
val network = feeCryptoCurrencyStatusProvider()?.currency?.network?.id?.value
return if (network != null && isBitcoin(network)) {
persistentListOf(
@ -47,7 +48,11 @@ internal class BitcoinCustomFeeConverter(
),
title = resourceReference(R.string.send_max_fee),
footer = resourceReference(R.string.send_max_fee_footer),
label = getFeeFormatted(feeValue),
label = getFiatReference(
rate = feeCurrency?.fiatRate,
value = feeValue,
appCurrency = appCurrencyProvider(),
),
keyboardActions = KeyboardActions(),
isReadonly = true,
),
@ -63,10 +68,20 @@ internal class BitcoinCustomFeeConverter(
footer = resourceReference(R.string.send_satoshi_per_byte_text),
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_SATOSHI_INDEX, it) },
keyboardOptions = KeyboardOptions(
imeAction = if (checkExceedBalance(feeValue)) ImeAction.None else ImeAction.Done,
imeAction = if (checkExceedBalance(
feeBalance = feeCurrency?.amount,
feeAmount = feeValue,
)
) {
ImeAction.None
} else {
ImeAction.Done
},
keyboardType = KeyboardType.Number,
),
keyboardActions = KeyboardActions(),
keyboardActions = KeyboardActions(
onDone = { clickIntents.onNextClick(stateRouterProvider().isEditState) },
),
),
)
} else {
@ -100,7 +115,11 @@ internal class BitcoinCustomFeeConverter(
FEE_AMOUNT_INDEX,
this[FEE_AMOUNT_INDEX].copy(
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT_INDEX].decimals),
label = getFeeFormatted(newFeeAmount),
label = getFiatReference(
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
value = newFeeAmount,
appCurrency = appCurrencyProvider(),
),
),
)
set(index, this[index].copy(value = value))
@ -108,26 +127,6 @@ internal class BitcoinCustomFeeConverter(
}.toImmutableList()
}
private fun getFeeFormatted(fee: BigDecimal?): TextReference {
val appCurrency = appCurrencyProvider()
val rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate
val fiatFee = rate?.let { fee?.multiply(it) }
return stringReference(
BigDecimalFormatter.formatFiatAmount(
fiatAmount = fiatFee,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
)
}
private fun checkExceedBalance(feeAmount: BigDecimal?): Boolean {
val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider()
val currencyCryptoAmount = cryptoCurrencyStatus?.value?.amount ?: BigDecimal.ZERO
return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount
}
private fun toSatoshiPerByte(amount: BigDecimal?, decimals: Int, txSize: BigDecimal): BigDecimal? {
val newFeeAmount = amount?.movePointRight(decimals)
return newFeeAmount?.divide(

View file

@ -5,33 +5,33 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.extensions.isZero
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.utils.getFiatReference
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
import java.math.RoundingMode
internal class EthereumCustomFeeConverter(
private val clickIntents: SendClickIntents,
private val stateRouterProvider: Provider<StateRouter>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
) : CustomFeeConverter<Fee.Ethereum> {
override fun convert(value: Fee.Ethereum): ImmutableList<SendTextField.CustomFee> {
val feeValue = value.amount.value
val feeCurrency = feeCryptoCurrencyStatusProvider()?.value
return persistentListOf(
SendTextField.CustomFee(
value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(),
@ -44,7 +44,11 @@ internal class EthereumCustomFeeConverter(
),
title = resourceReference(R.string.send_max_fee),
footer = resourceReference(R.string.send_max_fee_footer),
label = getFeeFormatted(feeValue),
label = getFiatReference(
rate = feeCurrency?.fiatRate,
value = feeValue,
appCurrency = appCurrencyProvider(),
),
keyboardActions = KeyboardActions(),
),
SendTextField.CustomFee(
@ -68,10 +72,21 @@ internal class EthereumCustomFeeConverter(
footer = resourceReference(R.string.send_gas_limit_footer),
onValueChange = { clickIntents.onCustomFeeValueChange(GAS_LIMIT, it) },
keyboardOptions = KeyboardOptions(
imeAction = if (checkExceedBalance(feeValue)) ImeAction.None else ImeAction.Done,
imeAction = if (
checkExceedBalance(
feeBalance = feeCurrency?.amount,
feeAmount = feeValue,
)
) {
ImeAction.None
} else {
ImeAction.Done
},
keyboardType = KeyboardType.Number,
),
keyboardActions = KeyboardActions(onDone = { clickIntents.onNextClick() }),
keyboardActions = KeyboardActions(
onDone = { clickIntents.onNextClick(stateRouterProvider().isEditState) },
),
),
)
}
@ -102,26 +117,6 @@ internal class EthereumCustomFeeConverter(
}.toImmutableList()
}
private fun getFeeFormatted(fee: BigDecimal?): TextReference {
val appCurrency = appCurrencyProvider()
val rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate
val fiatFee = rate?.let { fee?.multiply(it) }
return stringReference(
BigDecimalFormatter.formatFiatAmount(
fiatAmount = fiatFee,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
)
}
private fun checkExceedBalance(feeAmount: BigDecimal?): Boolean {
val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider()
val currencyCryptoAmount = cryptoCurrencyStatus?.value?.amount ?: BigDecimal.ZERO
return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount
}
private fun MutableList<SendTextField.CustomFee>.setEmpty(index: Int) {
set(index, this[index].copy(value = ""))
}
@ -140,7 +135,11 @@ internal class EthereumCustomFeeConverter(
index,
this[index].copy(
value = value,
label = getFeeFormatted(newFeeAmountDecimal),
label = getFiatReference(
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
value = newFeeAmountDecimal,
appCurrency = appCurrencyProvider(),
),
),
)
}
@ -159,7 +158,11 @@ internal class EthereumCustomFeeConverter(
FEE_AMOUNT,
this[FEE_AMOUNT].copy(
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals),
label = getFeeFormatted(newFeeAmount),
label = getFiatReference(
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
value = newFeeAmount,
appCurrency = appCurrencyProvider(),
),
),
)
set(index, this[index].copy(value = value))
@ -179,15 +182,23 @@ internal class EthereumCustomFeeConverter(
FEE_AMOUNT,
this[FEE_AMOUNT].copy(
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals),
label = getFeeFormatted(newFeeAmount),
label = getFiatReference(
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
value = newFeeAmount,
appCurrency = appCurrencyProvider(),
),
),
)
val isNotExceedBalance = checkExceedBalance(
feeBalance = feeCryptoCurrencyStatusProvider()?.value?.amount,
feeAmount = newFeeAmount,
)
set(
index,
this[index].copy(
value = value,
keyboardOptions = KeyboardOptions(
imeAction = if (!checkExceedBalance(newFeeAmount)) ImeAction.None else ImeAction.Done,
imeAction = if (!isNotExceedBalance) ImeAction.None else ImeAction.Done,
keyboardType = KeyboardType.Number,
),
),

View file

@ -1,18 +1,20 @@
package com.tangem.features.send.impl.presentation.state.fields
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.extensions.isZero
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.StateRouter
import com.tangem.features.send.impl.presentation.state.amount.checkExceedBalance
import com.tangem.features.send.impl.presentation.state.amount.getCryptoValue
import com.tangem.features.send.impl.presentation.state.amount.getFiatValue
import com.tangem.features.send.impl.presentation.state.amount.getKeyboardAction
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
import java.math.BigDecimal
import java.math.RoundingMode
internal class SendAmountFieldChangeConverter(
private val stateRouterProvider: Provider<StateRouter>,
@ -21,6 +23,7 @@ internal class SendAmountFieldChangeConverter(
) : Converter<String, SendUiState> {
override fun convert(value: String): SendUiState {
val state = currentStateProvider()
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val isEditState = stateRouterProvider().isEditState
val amountState = state.getAmountState(isEditState) ?: return state
val amountTextField = amountState.amountTextField
@ -30,14 +33,21 @@ internal class SendAmountFieldChangeConverter(
val fiatDecimals = amountTextField.fiatAmount.decimals
val trimmedValue = value.trim()
val cryptoValue = trimmedValue.getCryptoValue(amountTextField.isFiatValue, cryptoDecimals)
val fiatValue = trimmedValue.getFiatValue(amountTextField.isFiatValue, fiatDecimals)
val cryptoValue = trimmedValue.getCryptoValue(
fiatRate = cryptoCurrencyStatus.value.fiatRate,
isFiatValue = amountTextField.isFiatValue,
decimals = cryptoDecimals,
)
val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals)
val decimalFiatValue = fiatValue.parseToBigDecimal(fiatDecimals)
val (fiatValue, decimalFiatValue) = trimmedValue.getFiatValue(
fiatRate = cryptoCurrencyStatus.value.fiatRate,
isFiatValue = amountTextField.isFiatValue,
decimals = fiatDecimals,
)
val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue
val isExceedBalance = checkValue.checkExceedBalance(amountTextField)
val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isZero() else decimalCryptoValue.isZero()
val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField)
val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else decimalCryptoValue.isZero()
return state.copyWrapped(
isEditState = isEditState,
amountState = amountState.copy(
@ -57,27 +67,6 @@ internal class SendAmountFieldChangeConverter(
)
}
private fun String.getCryptoValue(isFiatValue: Boolean, decimals: Int): String {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val fiatRate = cryptoCurrencyStatus.value.fiatRate
return if (isFiatValue && fiatRate != null) {
parseToBigDecimal(decimals).divide(fiatRate, decimals, RoundingMode.DOWN)
.parseBigDecimal(decimals)
} else {
this
}
}
private fun String.getFiatValue(isFiatValue: Boolean, decimals: Int): String {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val fiatRate = cryptoCurrencyStatus.value.fiatRate
return if (!isFiatValue && fiatRate != null) {
parseToBigDecimal(decimals).multiply(fiatRate).parseBigDecimal(decimals)
} else {
this
}
}
private fun SendUiState.emptyState(): SendUiState {
val isEditState = stateRouterProvider().isEditState
val amountState = getAmountState(isEditState) ?: return this
@ -96,24 +85,4 @@ internal class SendAmountFieldChangeConverter(
),
)
}
private fun String.checkExceedBalance(amountTextField: SendTextField.AmountField): Boolean {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO
val fiatDecimal = parseToBigDecimal(amountTextField.fiatAmount.decimals)
val cryptoDecimal = parseToBigDecimal(amountTextField.cryptoAmount.decimals)
return if (amountTextField.isFiatValue) {
fiatDecimal > currencyFiatAmount
} else {
cryptoDecimal > currencyCryptoAmount
}
}
private fun getKeyboardAction(isExceedBalance: Boolean, decimalCryptoValue: BigDecimal) =
if (!isExceedBalance && !decimalCryptoValue.isZero()) {
ImeAction.Done
} else {
ImeAction.None
}
}

View file

@ -14,15 +14,18 @@ import com.tangem.domain.tokens.model.AmountType
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.convertToAmount
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
import java.math.BigDecimal
private const val FIAT_DECIMALS = 2
internal class SendAmountFieldConverter(
private val clickIntents: SendClickIntents,
private val stateRouterProvider: Provider<StateRouter>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val appCurrencyProvider: Provider<AppCurrency>,
) : Converter<String, SendTextField.AmountField> {
@ -32,12 +35,14 @@ internal class SendAmountFieldConverter(
val cryptoDecimal = value.toBigDecimalOrDefault()
val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency)
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val (fiatValue, fiatDecimal) = if (value.isEmpty()) {
"" to BigDecimal.ZERO
} else {
val fiatDecimal = fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO
val fiatValue = fiatDecimal.parseBigDecimal(FIAT_DECIMALS)
fiatValue to fiatDecimal
val (fiatValue, fiatDecimal) = when {
fiatRate.isNullOrZero() -> "" to null
value.isEmpty() -> "" to BigDecimal.ZERO
else -> {
val fiatDecimal = fiatRate?.multiply(cryptoDecimal)
val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty()
fiatValue to fiatDecimal
}
}
val isDoneActionEnabled = !cryptoDecimal.isZero()
return SendTextField.AmountField(
@ -48,7 +53,9 @@ internal class SendAmountFieldConverter(
imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None,
keyboardType = KeyboardType.Number,
),
keyboardActions = KeyboardActions(onDone = { clickIntents.onNextClick() }),
keyboardActions = KeyboardActions(
onDone = { clickIntents.onNextClick(stateRouterProvider().isEditState) },
),
isFiatValue = false,
cryptoAmount = cryptoAmount,
fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrencyProvider()),
@ -60,7 +67,7 @@ internal class SendAmountFieldConverter(
)
}
private fun getAppCurrencyAmount(fiatValue: BigDecimal, appCurrency: AppCurrency) = Amount(
private fun getAppCurrencyAmount(fiatValue: BigDecimal?, appCurrency: AppCurrency) = Amount(
currencySymbol = appCurrency.symbol,
value = fiatValue,
decimals = FIAT_DECIMALS,

View file

@ -49,6 +49,7 @@ internal object AmountStatePreviewData {
onValuePastedTriggerDismiss = {},
),
isSegmentedButtonsEnabled = true,
selectedButton = 0,
)
val fiatAmountState = amountState.copy(

View file

@ -14,6 +14,8 @@ internal object SendClickIntentsStub : SendClickIntents {
override fun onBackClick() {}
override fun onCloseClick() {}
override fun onNextClick(isFromEdit: Boolean) {}
override fun onPrevClick() {}
@ -58,7 +60,7 @@ internal object SendClickIntentsStub : SendClickIntents {
override fun onShareClick() {}
override fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class<out SendNotification>) {}
override fun onAmountReduceClick(reduceAmountBy: BigDecimal, clazz: Class<out SendNotification>) {}
override fun onNotificationCancel(clazz: Class<out SendNotification>) {}
}

View file

@ -165,7 +165,11 @@ private fun SendingText(
val fiatRate = feeState?.rate
val fiatAmount = amountState?.amountTextField?.fiatAmount
val feeFiat = fiatRate?.let { feeState.fee?.amount?.value?.multiply(it) }
val sendingFiat = feeFiat?.let { fiatAmount?.value?.plus(it) }
val sendingFiat = if (uiState.isSubtracted) {
fiatAmount?.value
} else {
feeFiat?.let { fiatAmount?.value?.plus(it) }
}
if (feeFiat != null && sendingFiat != null) {
val sendingValue = BigDecimalFormatter.formatFiatAmount(

View file

@ -33,7 +33,7 @@ import kotlinx.coroutines.flow.withIndex
@Composable
internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) {
val snackbarHostState = remember { SnackbarHostState() }
BackHandler { uiState.clickIntents.onBackClick() }
BackHandler(onBack = uiState.clickIntents::onBackClick)
Column(
modifier = Modifier
.fillMaxSize()
@ -88,17 +88,17 @@ private fun SendAppBar(uiState: SendUiState, currentState: SendUiCurrentScreen)
} else {
null
}
val (backIcon, backClick) = when (currentState.type) {
val backIcon = when (currentState.type) {
SendUiStateType.EditAmount,
SendUiStateType.EditFee,
SendUiStateType.EditRecipient,
-> R.drawable.ic_back_24 to uiState.clickIntents::onBackClick
else -> R.drawable.ic_close_24 to uiState.clickIntents::popBackStack
-> R.drawable.ic_back_24
else -> R.drawable.ic_close_24
}
AppBarWithBackButtonAndIcon(
text = titleRes?.resolveReference(),
subtitle = subtitleRes,
onBackClick = backClick,
onBackClick = uiState.clickIntents::onCloseClick,
onIconClick = uiState.clickIntents::onQrCodeScanClick,
backIconRes = backIcon,
iconRes = iconRes,

View file

@ -29,6 +29,7 @@ internal fun LazyListScope.buttons(
segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
clickIntents: SendClickIntents,
isSegmentedButtonsEnabled: Boolean,
selectedButton: Int,
) {
item(
key = AMOUNT_BUTTONS_KEY,
@ -48,6 +49,7 @@ internal fun LazyListScope.buttons(
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clickIntents.onCurrencyChangeClick(it.isFiat)
},
initialSelectedItem = segmentedButtonConfig.getOrNull(selectedButton),
isEnabled = isSegmentedButtonsEnabled,
) {
SendAmountCurrencyButton(

View file

@ -36,6 +36,7 @@ internal fun SendAmountContent(
segmentedButtonConfig = amountState.segmentedButtonConfig,
clickIntents = clickIntents,
isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled,
selectedButton = amountState.selectedButton,
)
}
}

View file

@ -54,7 +54,7 @@ internal fun SendSpeedSelectorItem(
onSelect = onSelect,
modifier = modifier,
preDot = getCryptoReference(amount, state.isFeeApproximate),
postDot = getFiatReference(amount, state.rate, state.appCurrency),
postDot = getFiatReference(amount?.value, state.rate, state.appCurrency),
ellipsizeOffset = amount?.currencySymbol?.length,
isSelected = content?.selectedFee == feeType,
showDivider = showDivider,

View file

@ -31,8 +31,8 @@ internal fun AmountBlock(
val amount = amountState.amountTextField
val cryptoAmount = BigDecimalFormatter.formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol)
val fiatAmount = BigDecimalFormatter.formatFiatEditableAmount(
fiatAmount = amount.fiatValue,
val fiatAmount = BigDecimalFormatter.formatFiatAmount(
fiatAmount = amount.fiatAmount.value,
fiatCurrencySymbol = amount.fiatAmount.currencySymbol,
fiatCurrencyCode = amountState.appCurrencyCode,
)

View file

@ -60,7 +60,7 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick
titleRes = title,
iconRes = icon,
preDot = getCryptoReference(feeAmount, feeState.isFeeApproximate),
postDot = feeAmount?.let { getFiatReference(it, feeState.rate, feeState.appCurrency) },
postDot = getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency),
ellipsizeOffset = feeAmount?.currencySymbol?.length,
isSelected = true,
showDivider = false,

View file

@ -22,11 +22,11 @@ internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): Tex
)
}
internal fun getFiatReference(amount: Amount?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? {
if (amount == null) return null
internal fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? {
if (value == null || rate == null) return null
return stringReference(
BigDecimalFormatter.formatFiatAmount(
fiatAmount = rate?.let { amount.value?.multiply(it) },
fiatAmount = value.multiply(rate),
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),

View file

@ -14,6 +14,8 @@ internal interface SendClickIntents {
fun onBackClick()
fun onCloseClick()
fun onNextClick(isFromEdit: Boolean = false)
fun onPrevClick()
@ -65,7 +67,7 @@ internal interface SendClickIntents {
fun onShareClick()
fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class<out SendNotification>)
fun onAmountReduceClick(reduceAmountBy: BigDecimal, clazz: Class<out SendNotification>)
fun onNotificationCancel(clazz: Class<out SendNotification>)
// endregion

View file

@ -47,7 +47,7 @@ import com.tangem.features.send.impl.navigation.InnerSendRouter
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
import com.tangem.features.send.impl.presentation.analytics.SendScreenSource
import com.tangem.features.send.impl.presentation.analytics.utils.SendOnNextScreenAnalyticSender
import com.tangem.features.send.impl.presentation.analytics.utils.SendScreenAnalyticSender
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
import com.tangem.features.send.impl.presentation.state.*
import com.tangem.features.send.impl.presentation.state.amount.AmountStateFactory
@ -174,9 +174,10 @@ internal class SendViewModel @Inject constructor(
getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase,
)
private val sendOnNextScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) {
SendOnNextScreenAnalyticSender(
private val sendScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) {
SendScreenAnalyticSender(
stateRouterProvider = Provider { stateRouter },
currentStateProvider = Provider { uiState },
analyticsEventHandler = analyticsEventHandler,
)
}
@ -464,7 +465,10 @@ internal class SendViewModel @Inject constructor(
SendUiStateType.Fee,
SendUiStateType.EditFee,
-> loadFee()
SendUiStateType.Send -> sendIdleTimer = SystemClock.elapsedRealtime()
SendUiStateType.Send -> {
uiState = stateFactory.getIsAmountSubtractedState(isAmountSubtractAvailable)
sendIdleTimer = SystemClock.elapsedRealtime()
}
else -> Unit
}
}
@ -498,10 +502,21 @@ internal class SendViewModel @Inject constructor(
stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true)
}
override fun onCloseClick() {
sendScreenAnalyticSender.sendOnClose()
when (stateRouter.currentState.value.type) {
SendUiStateType.EditAmount,
SendUiStateType.EditFee,
SendUiStateType.EditRecipient,
-> onBackClick()
else -> popBackStack()
}
}
override fun onNextClick(isFromEdit: Boolean) {
val currentState = stateRouter.currentState.value
uiState = stateFactory.syncEditStates(isFromEdit = isFromEdit)
sendOnNextScreenAnalyticSender.send(currentState.type, uiState)
sendScreenAnalyticSender.send(currentState.type, uiState)
when (currentState.type) {
SendUiStateType.Fee,
SendUiStateType.EditFee,
@ -652,7 +667,7 @@ internal class SendViewModel @Inject constructor(
private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) {
val isRecent = type == EnterAddressSource.RecentAddress
if (isRecent && isValidAddress) onNextClick()
if (isRecent && isValidAddress) onNextClick(stateRouter.isEditState)
}
// endregion
@ -687,6 +702,7 @@ internal class SendViewModel @Inject constructor(
val isShowStatus = uiState.feeState?.fee == null
if (isShowStatus) {
uiState = feeStateFactory.onFeeOnLoadingState()
updateNotifications()
}
val result = callFeeUseCase()?.fold(
ifRight = {
@ -722,14 +738,12 @@ internal class SendViewModel @Inject constructor(
val recipientState = uiState.getRecipientState(isFromConfirmation) ?: return null
val amount = amountState.amountTextField.cryptoAmount.value ?: return null
return feeCryptoCurrencyStatus?.let { feeCurrencyStatus ->
getFeeUseCase.invoke(
amount = amount,
destination = recipientState.addressTextField.value,
userWallet = userWallet,
cryptoCurrency = feeCurrencyStatus.currency,
)
}
return getFeeUseCase.invoke(
amount = amount,
destination = recipientState.addressTextField.value,
userWallet = userWallet,
cryptoCurrency = cryptoCurrencyStatus.currency,
)
}
// endregion
@ -783,8 +797,8 @@ internal class SendViewModel @Inject constructor(
analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked)
}
override fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class<out SendNotification>) {
uiState = amountStateFactory.getOnAmountValueChange(reducedAmount.parseBigDecimal(cryptoCurrency.decimals))
override fun onAmountReduceClick(reduceAmountBy: BigDecimal, clazz: Class<out SendNotification>) {
uiState = amountStateFactory.getOnAmountReducedState(reduceAmountBy)
uiState = sendNotificationFactory.dismissNotificationState(clazz)
feeReload()
}
@ -806,6 +820,7 @@ internal class SendViewModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = uiState.sendState?.reduceAmountBy,
)
viewModelScope.launch(dispatchers.main) {