Updated on 2026-08-14

This commit is contained in:
Tangem 2024-01-25 15:24:29 +03:00
commit 490f7cc330
6 changed files with 175 additions and 38 deletions

View file

@ -10,12 +10,16 @@ class PeriodicTask<T>(
private val task: suspend () -> Result<T>,
private val onSuccess: (T) -> Unit,
private val onError: (Throwable) -> Unit,
private val isDelayFirst: Boolean = false,
) {
private var isActive: AtomicBoolean = AtomicBoolean(false)
suspend fun runTaskWithDelay() {
isActive.set(true)
if (isDelayFirst) {
delay(delay)
}
while (isActive.get()) {
task.invoke()
.onSuccess {

View file

@ -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)
}
}

View file

@ -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(

View file

@ -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
}

View file

@ -55,6 +55,7 @@ internal fun TextFieldWithPaste(
placeholder = placeholder,
onValueChange = onValueChange,
modifier = Modifier
.weight(1f)
.padding(top = TangemTheme.dimens.spacing6),
)
}

View file

@ -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
}
}