Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-02 14:36:29 +05:00
commit 613bb9226f
8 changed files with 64 additions and 54 deletions

View file

@ -3,7 +3,6 @@ package com.tangem.features.send.impl.presentation.state
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory
@ -91,15 +90,6 @@ internal class SendEventStateFactory(
fun getFeeTooLowAlert(onConsume: () -> Unit): SendUiState {
val state = currentStateProvider()
val feeSelectorState = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return state
val minimumValue = multipleFees.minimum.amount.value ?: return state
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return state
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
val isFeeTooLow = feeSelectorState.selectedFee == FeeType.Custom && minimumValue > customValue
if (!isFeeTooLow) return state
return state.copy(
event = triggeredEvent(
data = SendEvent.ShowAlert(

View file

@ -101,8 +101,8 @@ data class SendUiCurrentScreen(
enum class SendUiStateType {
None,
Amount,
Recipient,
Amount,
Fee,
Send,
}

View file

@ -1,5 +1,7 @@
package com.tangem.features.send.impl.presentation.state.fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
@ -11,4 +13,17 @@ internal fun checkFeeCoverage(state: SendUiState, cryptoCurrencyStatus: CryptoCu
val fee = state.feeState?.fee?.amount?.value ?: return false
val amount = state.amountState?.amountTextField?.cryptoAmount?.value ?: return false
return balance <= amount + fee
}
/**
* Check if custom fee is too low
*/
internal fun checkIfFeeTooLow(state: SendUiState): Boolean {
val feeSelectorState = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return false
val minimumValue = multipleFees.minimum.amount.value ?: return false
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return false
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
return feeSelectorState.selectedFee == FeeType.Custom && minimumValue > customValue
}

View file

@ -57,7 +57,6 @@ internal class FeeStateFactory(
val feeState = state.feeState ?: return state
val feeSelectorState = (feeState.feeSelectorState as? FeeSelectorState.Content)?.copy(
fees = fees,
customValues = customFeeFieldConverter.convert(fees.normal),
) ?: FeeSelectorState.Content(
fees = fees,
customValues = customFeeFieldConverter.convert(fees.normal),

View file

@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -33,7 +32,7 @@ import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType
@Composable
internal fun SendNavigationButtons(uiState: SendUiState, currentState: State<SendUiCurrentScreen>) {
internal fun SendNavigationButtons(uiState: SendUiState, currentState: SendUiCurrentScreen) {
Row(
modifier = Modifier
.fillMaxWidth()
@ -54,11 +53,10 @@ internal fun SendNavigationButtons(uiState: SendUiState, currentState: State<Sen
}
@Composable
private fun SendSecondaryNavigationButton(uiState: SendUiState, currentState: State<SendUiCurrentScreen>) {
private fun SendSecondaryNavigationButton(uiState: SendUiState, currentState: SendUiCurrentScreen) {
val isEditingDisabled = uiState.isEditingDisabled
val isFromConfirmation = currentState.value.isFromConfirmation
val isCorrectScreen =
currentState.value.type == SendUiStateType.Amount || currentState.value.type == SendUiStateType.Fee
val isFromConfirmation = currentState.isFromConfirmation
val isCorrectScreen = currentState.type == SendUiStateType.Amount || currentState.type == SendUiStateType.Fee
AnimatedVisibility(
visible = !isEditingDisabled && isCorrectScreen && !isFromConfirmation,
enter = expandHorizontally(expandFrom = Alignment.End),
@ -83,7 +81,7 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState, currentState: St
@Composable
private fun SendPrimaryNavigationButton(
uiState: SendUiState,
currentState: State<SendUiCurrentScreen>,
currentState: SendUiCurrentScreen,
modifier: Modifier = Modifier,
) {
val isSuccess = uiState.sendState.isSuccess
@ -107,7 +105,7 @@ private fun SendPrimaryNavigationButton(
modifier = modifier,
) { textId ->
when {
currentState.value.type == SendUiStateType.Send && !isSuccess -> {
currentState.type == SendUiStateType.Send && !isSuccess -> {
val hapticFeedback = rememberHapticFeedback(state = currentState, onAction = buttonClick)
PrimaryButtonIconEnd(
text = stringResource(textId),
@ -117,7 +115,7 @@ private fun SendPrimaryNavigationButton(
showProgress = isSending,
)
}
currentState.value.type == SendUiStateType.Send && isSuccess -> {
currentState.type == SendUiStateType.Send && isSuccess -> {
PrimaryButtonsDone(
textRes = textId,
txUrl = txUrl,
@ -184,15 +182,15 @@ private fun PrimaryButtonsDone(
private fun getButtonData(
uiState: SendUiState,
currentState: State<SendUiCurrentScreen>,
currentState: SendUiCurrentScreen,
isSuccess: Boolean,
): Pair<Int, () -> Unit> {
return when (currentState.value.type) {
return when (currentState.type) {
SendUiStateType.None,
SendUiStateType.Amount,
SendUiStateType.Recipient,
SendUiStateType.Fee,
-> if (currentState.value.isFromConfirmation) {
-> if (currentState.isFromConfirmation) {
R.string.common_continue to uiState.clickIntents::onNextClick
} else {
R.string.common_next to uiState.clickIntents::onNextClick
@ -205,8 +203,8 @@ private fun getButtonData(
}
}
private fun isButtonEnabled(currentState: State<SendUiCurrentScreen>, uiState: SendUiState): Boolean {
return when (currentState.value.type) {
private fun isButtonEnabled(currentState: SendUiCurrentScreen, uiState: SendUiState): Boolean {
return when (currentState.type) {
SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false
SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled ?: false
SendUiStateType.Fee -> uiState.feeState?.isPrimaryButtonEnabled ?: false

View file

@ -1,16 +1,15 @@
package com.tangem.features.send.impl.presentation.ui
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ -63,11 +62,11 @@ internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow<SendUi
)
SendScreenContent(
uiState = uiState,
currentState = currentState,
currentState = currentState.value,
modifier = Modifier
.weight(1f),
)
SendNavigationButtons(uiState, currentState)
SendNavigationButtons(uiState, currentState.value)
}
SendEventEffect(
@ -77,15 +76,24 @@ internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow<SendUi
}
@Composable
private fun SendScreenContent(
uiState: SendUiState,
currentState: State<SendUiCurrentScreen>,
modifier: Modifier = Modifier,
) {
private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentScreen, modifier: Modifier = Modifier) {
var lastState by remember { mutableIntStateOf(currentState.type.ordinal) }
val direction = remember(currentState.type.ordinal) {
if (lastState < currentState.type.ordinal) {
AnimatedContentTransitionScope.SlideDirection.Start
} else {
AnimatedContentTransitionScope.SlideDirection.End
}
}
AnimatedContent(
targetState = currentState.value,
targetState = currentState,
label = "Send Scree Navigation",
modifier = modifier,
transitionSpec = {
lastState = currentState.type.ordinal
slideIntoContainer(towards = direction, animationSpec = tween())
.togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween()))
},
) { state ->
when (state.type) {
SendUiStateType.Amount -> SendAmountContent(

View file

@ -1,5 +1,6 @@
package com.tangem.features.send.impl.presentation.ui.fee
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -20,7 +21,10 @@ internal fun SendCustomFeeEthereum(
selectedFee: FeeType,
modifier: Modifier = Modifier,
) {
if (selectedFee == FeeType.Custom && customValues.isNotEmpty()) {
AnimatedVisibility(
visible = selectedFee == FeeType.Custom && customValues.isNotEmpty(),
label = "Custom Fee Selected Animation",
) {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = modifier,

View file

@ -40,10 +40,7 @@ import com.tangem.features.send.impl.presentation.analytics.utils.SendOnNextScre
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
import com.tangem.features.send.impl.presentation.state.fee.FeeNotificationFactory
import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.state.fee.checkFeeCoverage
import com.tangem.features.send.impl.presentation.state.fee.*
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -367,7 +364,7 @@ internal class SendViewModel @Inject constructor(
stateRouter.currentState
.onEach {
when (it.type) {
SendUiStateType.Fee -> if (!it.isFromConfirmation) loadFee()
SendUiStateType.Fee -> loadFee()
SendUiStateType.Send -> sendIdleTimer = System.currentTimeMillis()
else -> Unit
}
@ -402,9 +399,6 @@ internal class SendViewModel @Inject constructor(
val currentState = stateRouter.currentState.value
val isCurrentFee = currentState.type == SendUiStateType.Fee
if (isCurrentFee) {
uiState = eventStateFactory.getFeeTooLowAlert(
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
)
val isFeeCoverage = checkFeeCoverage(uiState, cryptoCurrencyStatus)
if (isAmountSubtractAvailable && isFeeCoverage) {
uiState = eventStateFactory.getFeeCoverageAlert(
@ -415,6 +409,12 @@ internal class SendViewModel @Inject constructor(
uiState = stateFactory.onSubtractSelect(false)
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
}
if (checkIfFeeTooLow(uiState)) {
uiState = eventStateFactory.getFeeTooLowAlert(
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
)
return
}
}
sendOnNextScreenAnalyticSender.send(currentState.type, uiState)
@ -555,7 +555,9 @@ internal class SendViewModel @Inject constructor(
private fun loadFee() {
viewModelScope.launch(dispatchers.main) {
uiState = feeStateFactory.onFeeOnLoadingState()
if (uiState.feeState?.fee == null) {
uiState = feeStateFactory.onFeeOnLoadingState()
}
uiState = callFeeUseCase()?.fold(
ifRight = feeStateFactory::onFeeOnLoadedState,
ifLeft = { feeStateFactory.onFeeOnErrorState() },
@ -600,24 +602,18 @@ internal class SendViewModel @Inject constructor(
}
override fun showAmount() {
uiState = stateFactory.onSubtractSelect(false)
stateRouter.showAmount(isFromConfirmation = true)
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
}
override fun showRecipient() {
uiState = stateFactory.onSubtractSelect(false)
stateRouter.showRecipient(isFromConfirmation = true)
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
}
override fun showFee() {
uiState = stateFactory.onSubtractSelect(false)
stateRouter.showFee(isFromConfirmation = true)
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
}
override fun showSend() {