Updated on 2026-08-14
This commit is contained in:
commit
62e5621379
44 changed files with 509 additions and 235 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ internal class StateRouter(
|
|||
when {
|
||||
isSuccess -> popBackStack()
|
||||
isEditingDisabled -> when (type) {
|
||||
SendUiStateType.Send -> showFee()
|
||||
SendUiStateType.EditFee -> showSend()
|
||||
else -> popBackStack()
|
||||
}
|
||||
else -> when (type) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,7 @@ internal class SendAmountStateConverter(
|
|||
),
|
||||
),
|
||||
isSegmentedButtonsEnabled = !noFeeRate,
|
||||
selectedButton = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ internal class SendConfirmStateConverter(
|
|||
transactionDate = 0L,
|
||||
txUrl = "",
|
||||
ignoreAmountReduce = false,
|
||||
reduceAmountBy = null,
|
||||
isFromConfirmation = true,
|
||||
showTapHelp = isTapHelpPreviewEnabledProvider(),
|
||||
notifications = persistentListOf(),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ internal object AmountStatePreviewData {
|
|||
onValuePastedTriggerDismiss = {},
|
||||
),
|
||||
isSegmentedButtonsEnabled = true,
|
||||
selectedButton = 0,
|
||||
)
|
||||
|
||||
val fiatAmountState = amountState.copy(
|
||||
|
|
|
|||
|
|
@ -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>) {}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ internal fun SendAmountContent(
|
|||
segmentedButtonConfig = amountState.segmentedButtonConfig,
|
||||
clickIntents = clickIntents,
|
||||
isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled,
|
||||
selectedButton = amountState.selectedButton,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue