Updated on 2026-08-14
This commit is contained in:
parent
e4c91780ed
commit
da8f4010e0
6 changed files with 175 additions and 38 deletions
|
|
@ -44,4 +44,9 @@ internal sealed class SendAlertState {
|
|||
override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title)
|
||||
override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message)
|
||||
}
|
||||
|
||||
object FeeIncreased : SendAlertState() {
|
||||
override val title: TextReference? = null
|
||||
override val message: TextReference = resourceReference(id = R.string.send_notification_high_fee_title)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,27 @@
|
|||
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.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.features.send.impl.presentation.state.fee.*
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.getFee
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Factory to produce event state for [SendUiState]
|
||||
*
|
||||
* @param currentStateProvider [Provider] of [SendUiState]
|
||||
* @param clickIntents [SendClickIntents]
|
||||
* @param feeStateFactory [FeeStateFactory]
|
||||
*/
|
||||
internal class SendEventStateFactory(
|
||||
val currentStateProvider: Provider<SendUiState>,
|
||||
val clickIntents: SendClickIntents,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val feeStateFactory: FeeStateFactory,
|
||||
) {
|
||||
private val sendTransactionErrorConverter by lazy { SendTransactionAlertConverter(clickIntents) }
|
||||
|
||||
|
|
@ -34,6 +41,36 @@ internal class SendEventStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun getFeeUpdatedAlert(fee: TransactionFee, onConsume: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeSelector = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val newFee = when (fee) {
|
||||
is TransactionFee.Single -> fee.normal
|
||||
is TransactionFee.Choosable -> {
|
||||
when (feeSelector.selectedFee) {
|
||||
FeeType.SLOW -> fee.minimum
|
||||
FeeType.MARKET -> fee.normal
|
||||
FeeType.FAST -> fee.priority
|
||||
FeeType.CUSTOM -> return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val newFeeValue = newFee.amount.value ?: BigDecimal.ZERO
|
||||
val oldFeeValue = feeSelector.getFee().amount.value ?: BigDecimal.ZERO
|
||||
val updateFeeState = feeStateFactory.onFeeOnLoadedState(fee)
|
||||
return if (newFeeValue > oldFeeValue) {
|
||||
updateFeeState.copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(SendAlertState.FeeIncreased),
|
||||
onConsume = onConsume,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
updateFeeState
|
||||
}
|
||||
}
|
||||
|
||||
fun getGenericErrorState(error: Throwable? = null, onConsume: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
|
|
|
|||
|
|
@ -55,7 +55,10 @@ internal class FeeStateFactory(
|
|||
val state = currentStateProvider()
|
||||
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = FeeSelectorState.Content(
|
||||
val feeSelectorState = (feeState.feeSelectorState as? FeeSelectorState.Content)?.copy(
|
||||
fees = fees,
|
||||
customValues = customFeeFieldConverter.convert(fees.normal),
|
||||
) ?: FeeSelectorState.Content(
|
||||
fees = fees,
|
||||
customValues = customFeeFieldConverter.convert(fees.normal),
|
||||
)
|
||||
|
|
@ -182,14 +185,17 @@ internal class FeeStateFactory(
|
|||
return when (feeSelectorState) {
|
||||
is FeeSelectorState.Content -> {
|
||||
val customValue = feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull()
|
||||
val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
val fee = feeSelectorState.getFee().amount.value ?: BigDecimal.ZERO
|
||||
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
val fee = feeSelectorState.getFee()
|
||||
val feeValue = fee.amount.value ?: BigDecimal.ZERO
|
||||
|
||||
val isNotCustom = feeSelectorState.selectedFee != FeeType.CUSTOM
|
||||
val isNotEmptyCustom = !customValue.isNullOrZero() && !isNotCustom
|
||||
val isSubtractRequired = if (fee + receivedAmountValue >= balance) isSubtract else true
|
||||
val isBalanceEnough = fee + receivedAmountValue <= balance
|
||||
isBalanceEnough && isSubtractRequired && (isNotEmptyCustom || isNotCustom)
|
||||
val isFiatAnotherCurrency = cryptoCurrencyStatusProvider().currency.symbol != fee.amount.currencySymbol
|
||||
val isSubtractRequired = if (feeValue + receivedAmountValue >= balance) isSubtract else true
|
||||
val isBalanceEnough = feeValue + receivedAmountValue <= balance
|
||||
|
||||
(isFiatAnotherCurrency || isBalanceEnough && isSubtractRequired) && (isNotEmptyCustom || isNotCustom)
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ internal fun TextFieldWithPaste(
|
|||
placeholder = placeholder,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(top = TangemTheme.dimens.spacing6),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,20 +5,24 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import androidx.paging.PagingData
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpAddressService
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.utils.convertToAmount
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
|
|
@ -39,15 +43,10 @@ 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.getFee
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import com.tangem.utils.coroutines.*
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
|
@ -58,6 +57,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
|
||||
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
|
||||
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
|
|
@ -98,20 +98,19 @@ internal class SendViewModel @Inject constructor(
|
|||
getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase,
|
||||
)
|
||||
|
||||
private val feeStateFactory by lazy {
|
||||
FeeStateFactory(
|
||||
clickIntents = this,
|
||||
currentStateProvider = Provider { uiState },
|
||||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
userWalletProvider = Provider { userWallet },
|
||||
)
|
||||
}
|
||||
private val feeStateFactory = FeeStateFactory(
|
||||
clickIntents = this,
|
||||
currentStateProvider = Provider { uiState },
|
||||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
userWalletProvider = Provider { userWallet },
|
||||
)
|
||||
|
||||
private val eventStateFactory = SendEventStateFactory(
|
||||
clickIntents = this,
|
||||
currentStateProvider = Provider { uiState },
|
||||
feeStateFactory = feeStateFactory,
|
||||
)
|
||||
|
||||
private val sendNotificationFactory = SendNotificationFactory(
|
||||
|
|
@ -138,9 +137,17 @@ internal class SendViewModel @Inject constructor(
|
|||
private var sendNotificationsJobHolder = JobHolder()
|
||||
private var qrScannerJobHolder = JobHolder()
|
||||
|
||||
private var checkFeeUpdateScheduler = SingleTaskScheduler<Either<GetFeeError, TransactionFee>?>()
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
subscribeOnCurrencyStatusUpdates(owner)
|
||||
onFeeStateActive()
|
||||
onCheckFeeUpdate()
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
super.onStop(owner)
|
||||
checkFeeUpdateScheduler.cancelTask()
|
||||
}
|
||||
|
||||
fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) {
|
||||
|
|
@ -429,26 +436,30 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
private fun loadFee() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
val amountState = uiState.amountState ?: return@launch
|
||||
val recipientState = uiState.recipientState ?: return@launch
|
||||
val amount = amountState.amountTextField.value.toBigDecimal()
|
||||
|
||||
uiState = feeStateFactory.onFeeOnLoadingState()
|
||||
getFeeUseCase.invoke(
|
||||
amount = amount,
|
||||
destination = recipientState.addressTextField.value,
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
).fold(
|
||||
uiState = callFeeUseCase()?.fold(
|
||||
ifRight = {
|
||||
uiState = feeStateFactory.onFeeOnLoadedState(it)
|
||||
feeStateFactory.onFeeOnLoadedState(it)
|
||||
},
|
||||
ifLeft = {
|
||||
uiState = feeStateFactory.onFeeOnErrorState()
|
||||
feeStateFactory.onFeeOnErrorState()
|
||||
},
|
||||
)
|
||||
) ?: feeStateFactory.onFeeOnErrorState()
|
||||
}.saveIn(feeJobHolder)
|
||||
}
|
||||
|
||||
private suspend fun callFeeUseCase(): Either<GetFeeError, TransactionFee>? {
|
||||
val amountState = uiState.amountState ?: return null
|
||||
val recipientState = uiState.recipientState ?: return null
|
||||
val amount = amountState.amountTextField.value.toBigDecimal()
|
||||
|
||||
return getFeeUseCase.invoke(
|
||||
amount = amount,
|
||||
destination = recipientState.addressTextField.value,
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region send state clicks
|
||||
|
|
@ -527,13 +538,86 @@ internal class SendViewModel @Inject constructor(
|
|||
ifRight = {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState = stateFactory.getTransactionSendState(txData)
|
||||
scheduleBalanceUpdate()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun scheduleBalanceUpdate() {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
delay(BALANCE_UPDATE_DELAY)
|
||||
fetchCurrencyStatusUseCase.invoke(
|
||||
userWalletId = userWalletId,
|
||||
id = cryptoCurrency.id,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCheckFeeUpdate() {
|
||||
uiState.currentState
|
||||
.onEach { activeState ->
|
||||
val isSending = uiState.sendState.isSending
|
||||
val isSuccess = uiState.sendState.isSuccess
|
||||
val isNoNotifications = uiState.sendState.notifications.isEmpty()
|
||||
val isSendIdle = !isSending && !isSuccess && isNoNotifications
|
||||
if (activeState == SendUiStateType.Send && isSendIdle) {
|
||||
checkFeeUpdateScheduler.scheduleTask(
|
||||
scope = viewModelScope,
|
||||
task = getFeePeriodicTask(),
|
||||
)
|
||||
} else {
|
||||
checkFeeUpdateScheduler.cancelTask()
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun getFeePeriodicTask() = PeriodicTask(
|
||||
delay = CHECK_FEE_UPDATE_DELAY,
|
||||
isDelayFirst = true,
|
||||
task = {
|
||||
runCatching(dispatchers.main) {
|
||||
if (uiState.sendState.isSuccess) {
|
||||
checkFeeUpdateScheduler.cancelTask()
|
||||
null
|
||||
} else {
|
||||
callFeeUseCase()
|
||||
}
|
||||
}
|
||||
},
|
||||
onSuccess = { maybeFee ->
|
||||
if (maybeFee == null) {
|
||||
checkFeeUpdateScheduler.cancelTask()
|
||||
uiState = eventStateFactory.getGenericErrorState(
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
} else {
|
||||
maybeFee.fold(
|
||||
ifRight = {
|
||||
uiState = eventStateFactory.getFeeUpdatedAlert(
|
||||
fee = it,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
},
|
||||
ifLeft = {
|
||||
checkFeeUpdateScheduler.cancelTask()
|
||||
uiState = eventStateFactory.getGenericErrorState(
|
||||
error = (it as? GetFeeError.DataError)?.cause,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = { /* no-op */ },
|
||||
)
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
private const val XRP_X_ADDRESS = 'X'
|
||||
private const val DEFAULT_VALUE = "0.00"
|
||||
private const val CHECK_FEE_UPDATE_DELAY = 60_000L
|
||||
private const val BALANCE_UPDATE_DELAY = 10_000L
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue