From da8f4010e00536c5844d33a08cbb2e4fd0109aca Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Jan 2024 13:13:33 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/utils/coroutines/PeriodicTask.kt | 4 + .../impl/presentation/state/SendAlertState.kt | 5 + .../state/SendEventStateFactory.kt | 41 ++++- .../presentation/state/fee/FeeStateFactory.kt | 18 ++- .../ui/recipient/TextFieldWithPaste.kt | 1 + .../presentation/viewmodel/SendViewModel.kt | 144 ++++++++++++++---- 6 files changed, 175 insertions(+), 38 deletions(-) diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt index 5db470b916..3beae6ac03 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt @@ -10,12 +10,16 @@ class PeriodicTask( private val task: suspend () -> Result, 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 { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt index ff2aac684f..54eb2f7868 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt @@ -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) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt index 8bd869cb7c..794f00706a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt @@ -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, - val clickIntents: SendClickIntents, + private val currentStateProvider: Provider, + 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( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index abeea708ac..9ad8b30568 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -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 } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt index 8b3c83bfb0..b01980e9e9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -55,6 +55,7 @@ internal fun TextFieldWithPaste( placeholder = placeholder, onValueChange = onValueChange, modifier = Modifier + .weight(1f) .padding(top = TangemTheme.dimens.spacing6), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 7b406ca2d1..57d9dae131 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -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?>() + 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? { + 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 } } \ No newline at end of file