Updated on 2026-08-14
This commit is contained in:
commit
613bb9226f
8 changed files with 64 additions and 54 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -101,8 +101,8 @@ data class SendUiCurrentScreen(
|
|||
|
||||
enum class SendUiStateType {
|
||||
None,
|
||||
Amount,
|
||||
Recipient,
|
||||
Amount,
|
||||
Fee,
|
||||
Send,
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue