Updated on 2026-08-14

This commit is contained in:
Tangem 2024-01-29 16:53:00 +03:00
parent 885db20951
commit cd61d9c841
9 changed files with 369 additions and 143 deletions

View file

@ -310,4 +310,22 @@ internal object TokensDomainModule {
): GetNetworksSupportedByWallet {
return GetNetworksSupportedByWallet(repository = repository)
}
@Provides
@ViewModelScoped
fun provideGetBalanceNotEnoughForFeeWarningUseCase(
currenciesRepository: CurrenciesRepository,
dispatchers: CoroutineDispatcherProvider,
): GetBalanceNotEnoughForFeeWarningUseCase {
return GetBalanceNotEnoughForFeeWarningUseCase(currenciesRepository, dispatchers)
}
@Provides
@ViewModelScoped
fun provideIsAmountSubtractAvailableUseCase(
currenciesRepository: CurrenciesRepository,
dispatchers: CoroutineDispatcherProvider,
): IsAmountSubtractAvailableUseCase {
return IsAmountSubtractAvailableUseCase(currenciesRepository, dispatchers)
}
}

View file

@ -0,0 +1,97 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import java.math.BigDecimal
/**
* Use case for getting balance not enough warning to cover fee.
*
* This warning is shown when current currency is not paying fee and paying fee currency balance is not enough
*
* Current | Paying fee | Warning
* Coin | Coin | -
* Token | Coin | +
* Coin | PToken | + (VTO - VTHO)
* Token | PToken | + (Other VeChainToken - VTHO)
* PToken | PToken | - (VTHO - VTHO or TerraToken - TerraToken)
*/
class GetBalanceNotEnoughForFeeWarningUseCase(
private val currenciesRepository: CurrenciesRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(
fee: BigDecimal,
userWalletId: UserWalletId,
tokenStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus,
): Either<Throwable, CryptoCurrencyWarning?> = Either.catch {
withContext(dispatchers.io) {
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, tokenStatus.currency)
val coinBalance = coinStatus.value.amount ?: BigDecimal.ZERO
val isFeePaidByCoin = tokenStatus.currency is CryptoCurrency.Token
val isFeePaidByToken =
feePaidCurrency is FeePaidCurrency.Token && tokenStatus.currency.id != feePaidCurrency.tokenId
val warning = when {
feePaidCurrency is FeePaidCurrency.Coin && isFeePaidByCoin && fee > coinBalance -> {
CryptoCurrencyWarning.BalanceNotEnoughForFee(
tokenCurrency = tokenStatus.currency,
coinCurrency = coinStatus.currency,
)
}
feePaidCurrency is FeePaidCurrency.Token && isFeePaidByToken && fee > feePaidCurrency.balance -> {
constructTokenBalanceNotEnoughWarning(
userWalletId = userWalletId,
tokenStatus = tokenStatus,
feePaidToken = feePaidCurrency,
)
}
else -> null
}
warning
}
}
/**
* Check if fee paying token [feePaidToken] is added to wallet [userWalletId]
*/
private suspend fun constructTokenBalanceNotEnoughWarning(
userWalletId: UserWalletId,
tokenStatus: CryptoCurrencyStatus,
feePaidToken: FeePaidCurrency.Token,
): CryptoCurrencyWarning {
val token = currenciesRepository
.getMultiCurrencyWalletCurrenciesSync(userWalletId)
.find {
it is CryptoCurrency.Token &&
it.contractAddress.equals(feePaidToken.contractAddress, ignoreCase = true) &&
it.network.derivationPath == tokenStatus.currency.network.derivationPath
}
return if (token != null) {
CryptoCurrencyWarning.CustomTokenNotEnoughForFee(
currency = tokenStatus.currency,
feeCurrency = token,
networkName = token.network.name,
feeCurrencyName = feePaidToken.name,
feeCurrencySymbol = feePaidToken.symbol,
)
} else {
CryptoCurrencyWarning.CustomTokenNotEnoughForFee(
currency = tokenStatus.currency,
feeCurrency = null,
networkName = tokenStatus.currency.network.name,
feeCurrencyName = feePaidToken.name,
feeCurrencySymbol = feePaidToken.symbol,
)
}
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
/**
* Use case for checking if currency amount can be subtracted.
* Amount can be subtracted if only it is paying fee
*/
class IsAmountSubtractAvailableUseCase(
private val currenciesRepository: CurrenciesRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either<Throwable, Boolean> =
Either.catch {
withContext(dispatchers.io) {
when (val feeCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, currency)) {
is FeePaidCurrency.Coin -> currency is CryptoCurrency.Coin
is FeePaidCurrency.SameCurrency -> true
is FeePaidCurrency.Token -> currency.id == feeCurrency.tokenId
}
}
}
}

View file

@ -68,6 +68,7 @@ internal sealed class SendStates {
override val type: SendUiStateType = SendUiStateType.Fee,
override val isPrimaryButtonEnabled: Boolean = false,
val feeSelectorState: FeeSelectorState,
val isSubtractAvailable: Boolean,
val isSubtract: Boolean,
val isUserSubtracted: Boolean,
val fee: Fee?,

View file

@ -3,39 +3,53 @@ package com.tangem.features.send.impl.presentation.state.fee
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import java.math.BigDecimal
internal class FeeNotificationFactory(
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val currentStateProvider: Provider<SendUiState>,
private val userWalletProvider: Provider<UserWallet>,
private val clickIntents: SendClickIntents,
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
) {
operator fun invoke(feeState: SendStates.FeeState, amountValue: BigDecimal?): ImmutableList<SendFeeNotification> =
buildList {
when (val feeSelectorState = feeState.feeSelectorState) {
FeeSelectorState.Loading -> Unit
FeeSelectorState.Error -> {
addFeeUnreachableNotification(feeSelectorState)
fun create() = currentStateProvider().currentState
.filter { it == SendUiStateType.Fee }
.map {
val state = currentStateProvider()
val feeState = state.feeState ?: return@map persistentListOf()
buildList {
when (val feeSelectorState = feeState.feeSelectorState) {
FeeSelectorState.Loading -> Unit
FeeSelectorState.Error -> {
addFeeUnreachableNotification(feeSelectorState)
}
is FeeSelectorState.Content -> {
val customFee = feeSelectorState.customValues
val selectedFee = feeSelectorState.selectedFee
addTooLowNotification(feeSelectorState.fees, selectedFee, customFee)
addTooHighNotification(feeSelectorState.fees, selectedFee, customFee)
addFeeCoverageNotification(feeState, state.amountState)
addExceedsBalanceNotification(feeState.fee)
}
}
is FeeSelectorState.Content -> {
val customFee = feeSelectorState.customValues
val selectedFee = feeSelectorState.selectedFee
addTooLowNotification(feeSelectorState.fees, selectedFee, customFee)
addTooHighNotification(feeSelectorState.fees, selectedFee, customFee)
addFeeCoverageNotification(feeState, amountValue)
addExceedsBalanceNotification(feeState.fee)
}
}
}.toImmutableList()
}.toImmutableList()
}
private fun MutableList<SendFeeNotification>.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) {
if (feeSelectorState is FeeSelectorState.Error) {
@ -72,32 +86,60 @@ internal class FeeNotificationFactory(
private fun MutableList<SendFeeNotification>.addFeeCoverageNotification(
feeState: SendStates.FeeState,
amountValue: BigDecimal?,
amountState: SendStates.AmountState?,
) {
val cryptoAmount = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO
val value = amountValue ?: BigDecimal.ZERO
if (!feeState.isSubtractAvailable) return
val cryptoAmount = coinCryptoCurrencyStatusProvider().value.amount ?: return
val feeValue = feeState.fee?.amount?.value ?: return
val value = amountState?.amountTextField?.cryptoAmount?.value ?: return
if (cryptoAmount <= value + feeValue && feeState.isSubtract && !feeState.isUserSubtracted) {
add(SendFeeNotification.Warning.NetworkCoverage)
}
}
private fun MutableList<SendFeeNotification>.addExceedsBalanceNotification(fee: Fee?) {
val coinCryptoCurrency = coinCryptoCurrencyStatusProvider()
val cryptoAmount = coinCryptoCurrency.value.amount ?: BigDecimal.ZERO
private suspend fun MutableList<SendFeeNotification>.addExceedsBalanceNotification(fee: Fee?) {
val feeValue = fee?.amount?.value ?: BigDecimal.ZERO
val userWalletId = userWalletProvider().walletId
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
if (feeValue > cryptoAmount) {
add(
SendFeeNotification.Error.ExceedsBalance(
coinCryptoCurrency.currency.networkIconResId,
) {
clickIntents.onTokenDetailsClick(
userWalletProvider().walletId,
coinCryptoCurrency.currency,
)
},
)
val warning = getBalanceNotEnoughForFeeWarningUseCase(
fee = feeValue,
userWalletId = userWalletId,
tokenStatus = cryptoCurrencyStatus,
coinStatus = coinCryptoCurrencyStatusProvider(),
).fold(
ifLeft = { null },
ifRight = { it },
) ?: return
when (warning) {
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> {
add(
SendFeeNotification.Error.ExceedsBalance(
warning.coinCurrency.networkIconResId,
) {
clickIntents.onTokenDetailsClick(
userWalletProvider().walletId,
warning.coinCurrency,
)
},
)
}
is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> {
val currency = warning.feeCurrency ?: warning.currency
add(
SendFeeNotification.Error.ExceedsBalance(
currency.networkIconResId,
) {
clickIntents.onTokenDetailsClick(
userWalletId,
currency,
)
},
)
}
else -> Unit
}
}

View file

@ -5,12 +5,12 @@ import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.isNullOrZero
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
@ -24,7 +24,6 @@ internal class FeeStateFactory(
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val appCurrencyProvider: Provider<AppCurrency>,
userWalletProvider: Provider<UserWallet>,
) {
private val customFeeFieldConverter by lazy {
SendFeeCustomFieldConverter(
@ -33,12 +32,6 @@ internal class FeeStateFactory(
)
}
private val feeNotificationFactory = FeeNotificationFactory(
coinCryptoCurrencyStatusProvider = coinCryptoCurrencyStatusProvider,
userWalletProvider = userWalletProvider,
clickIntents = clickIntents,
)
fun onFeeOnLoadingState(): SendUiState {
val state = currentStateProvider()
val feeState = state.feeState ?: return state
@ -46,12 +39,12 @@ internal class FeeStateFactory(
feeState = feeState.copy(
feeSelectorState = FeeSelectorState.Loading,
notifications = persistentListOf(),
isPrimaryButtonEnabled = feeState.isPrimaryButtonEnabled(),
isPrimaryButtonEnabled = false,
),
)
}
fun onFeeOnLoadedState(fees: TransactionFee): SendUiState {
fun onFeeOnLoadedState(fees: TransactionFee, isSubtractAvailable: Boolean): SendUiState {
val state = currentStateProvider()
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
val feeState = state.feeState ?: return state
@ -65,35 +58,46 @@ internal class FeeStateFactory(
val fee = feeSelectorState.getFee()
val receivedAmount = calculateReceiveAmount(state, fee)
val updatedState = feeState.copy(
feeSelectorState = feeSelectorState,
fee = fee,
receivedAmountValue = receivedAmount,
receivedAmount = getFormattedValue(receivedAmount),
isSubtract = checkAutoSubtract(state, fee, balance),
)
return state.copy(
feeState = updatedState.copy(
notifications = feeNotificationFactory(
feeState = updatedState,
amountValue = state.amountState?.amountTextField?.cryptoAmount?.value,
),
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(),
feeState = feeState.copy(
isSubtractAvailable = isSubtractAvailable,
feeSelectorState = feeSelectorState,
fee = fee,
receivedAmountValue = receivedAmount,
receivedAmount = getFormattedValue(receivedAmount),
isSubtract = isSubtractAvailable && checkAutoSubtract(state, fee, balance),
),
)
}
fun onFeeOnLoadedState(fees: TransactionFee): SendUiState {
val state = currentStateProvider()
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
val feeState = state.feeState ?: return state
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
val updatedFeeSelector = feeSelectorState.copy(
fees = fees,
customValues = customFeeFieldConverter.convert(fees.normal),
)
val fee = updatedFeeSelector.getFee()
val receivedAmount = calculateReceiveAmount(state, fee)
return state.copy(
feeState = feeState.copy(
feeSelectorState = updatedFeeSelector,
fee = fee,
receivedAmountValue = receivedAmount,
receivedAmount = getFormattedValue(receivedAmount),
isSubtract = checkAutoSubtract(state, fee, balance),
),
)
}
fun onFeeOnErrorState(): SendUiState {
val state = currentStateProvider()
val updatedState = state.feeState?.copy(
feeSelectorState = FeeSelectorState.Error,
)
return state.copy(
feeState = updatedState?.copy(
notifications = feeNotificationFactory(
feeState = updatedState,
amountValue = state.amountState?.amountTextField?.cryptoAmount?.value,
),
feeState = state.feeState?.copy(
feeSelectorState = FeeSelectorState.Error,
),
)
}
@ -107,21 +111,13 @@ internal class FeeStateFactory(
val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType)
val fee = updatedFeeSelectorState.getFee()
val receivedAmount = calculateReceiveAmount(state, fee)
val updatedState = feeState.copy(
fee = fee,
feeSelectorState = updatedFeeSelectorState,
receivedAmountValue = receivedAmount,
receivedAmount = getFormattedValue(receivedAmount),
isSubtract = checkAutoSubtract(state, fee, balance),
)
return state.copy(
feeState = updatedState.copy(
notifications = feeNotificationFactory(
feeState = updatedState,
amountValue = state.amountState?.amountTextField?.cryptoAmount?.value,
),
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(),
feeState = feeState.copy(
fee = fee,
feeSelectorState = updatedFeeSelectorState,
receivedAmountValue = receivedAmount,
receivedAmount = getFormattedValue(receivedAmount),
isSubtract = checkAutoSubtract(state, fee, balance),
),
)
}
@ -139,20 +135,13 @@ internal class FeeStateFactory(
val fee = updatedFeeSelectorState.getFee()
val receivedAmount = calculateReceiveAmount(state, fee)
val updatedState = feeState.copy(
feeSelectorState = updatedFeeSelectorState,
fee = fee,
receivedAmountValue = receivedAmount,
receivedAmount = getFormattedValue(receivedAmount),
isSubtract = checkAutoSubtract(state, fee, balance),
)
return state.copy(
feeState = updatedState.copy(
notifications = feeNotificationFactory(
feeState = updatedState,
amountValue = state.amountState?.amountTextField?.cryptoAmount?.value,
),
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(),
feeState = feeState.copy(
feeSelectorState = updatedFeeSelectorState,
fee = fee,
receivedAmountValue = receivedAmount,
receivedAmount = getFormattedValue(receivedAmount),
isSubtract = checkAutoSubtract(state, fee, balance),
),
)
}
@ -163,42 +152,47 @@ internal class FeeStateFactory(
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
val fee = feeSelectorState.getFee()
val receivedAmount = calculateReceiveAmount(state, fee)
val updatedState = feeState.copy(
isSubtract = value,
isUserSubtracted = true,
receivedAmountValue = receivedAmount,
receivedAmount = getFormattedValue(receivedAmount),
fee = fee,
)
return state.copy(
feeState = updatedState.copy(
notifications = feeNotificationFactory(
feeState = updatedState,
amountValue = state.amountState?.amountTextField?.cryptoAmount?.value,
),
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(),
feeState = feeState.copy(
isSubtract = value,
isUserSubtracted = true,
receivedAmountValue = receivedAmount,
receivedAmount = getFormattedValue(receivedAmount),
fee = fee,
),
)
}
private fun SendStates.FeeState.isPrimaryButtonEnabled(): Boolean {
return when (feeSelectorState) {
is FeeSelectorState.Content -> {
val customValue = feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull()
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
val fee = feeSelectorState.getFee()
val feeValue = fee.amount.value ?: BigDecimal.ZERO
fun getFeeNotificationState(notifications: ImmutableList<SendFeeNotification>): SendUiState {
val state = currentStateProvider()
return state.copy(
feeState = state.feeState?.copy(
notifications = notifications,
isPrimaryButtonEnabled = isPrimaryButtonEnabled(state.feeState, notifications),
),
)
}
val isNotCustom = feeSelectorState.selectedFee != FeeType.CUSTOM
val isNotEmptyCustom = !customValue.isNullOrZero() && !isNotCustom
val isFiatAnotherCurrency = cryptoCurrencyStatusProvider().currency.symbol != fee.amount.currencySymbol
val isSubtractRequired = if (feeValue + receivedAmountValue >= balance) isSubtract else true
val isBalanceEnough = feeValue + receivedAmountValue <= balance
private fun isPrimaryButtonEnabled(
feeState: SendStates.FeeState,
notifications: ImmutableList<SendFeeNotification>,
): Boolean {
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return false
val customValue = feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull()
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
val fee = feeSelectorState.getFee()
val feeValue = fee.amount.value ?: BigDecimal.ZERO
(isFiatAnotherCurrency || isBalanceEnough && isSubtractRequired) && (isNotEmptyCustom || isNotCustom)
}
else -> false
val isNotCustom = feeSelectorState.selectedFee != FeeType.CUSTOM
val isNotEmptyCustom = !customValue.isNullOrZero() && !isNotCustom
val noErrors = notifications.none { it is SendFeeNotification.Error }
val isSubtractRequired = when {
!feeState.isSubtractAvailable -> true // current currency is not fee currency
feeValue + feeState.receivedAmountValue >= balance -> feeState.isSubtract
else -> feeValue + feeState.receivedAmountValue <= balance
}
return noErrors && isSubtractRequired && (isNotEmptyCustom || isNotCustom)
}
private fun checkAutoSubtract(state: SendUiState, fee: Fee, balance: BigDecimal): Boolean {

View file

@ -10,6 +10,7 @@ internal class SendFeeStateConverter : Converter<Unit, SendStates.FeeState> {
override fun convert(value: Unit): SendStates.FeeState {
return SendStates.FeeState(
feeSelectorState = FeeSelectorState.Loading,
isSubtractAvailable = false,
isSubtract = false,
isUserSubtracted = false,
fee = null,

View file

@ -22,6 +22,7 @@ import kotlinx.collections.immutable.ImmutableList
private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY"
private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY"
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: SendClickIntents) {
if (state == null) return
@ -41,6 +42,7 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S
SendSpeedSelector(
state = feeSendState,
clickIntents = clickIntents,
modifier = Modifier.animateItemPlacement(),
)
}
customFee(feeSendState)
@ -48,6 +50,7 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S
subtractButton(
receivedAmount = state.receivedAmount,
isSubtract = state.isSubtract,
isSubtractAvailable = state.isSubtractAvailable,
clickIntents = clickIntents,
)
}
@ -73,6 +76,13 @@ internal fun LazyListScope.notifications(configs: ImmutableList<SendFeeNotificat
},
iconTint = when (it) {
is SendFeeNotification.Informational -> TangemTheme.colors.icon.accent
is SendFeeNotification.Error.ExceedsBalance -> {
if (it.config.buttonsState == null) {
TangemTheme.colors.icon.warning
} else {
null
}
}
else -> null
},
)
@ -108,17 +118,20 @@ internal fun LazyListScope.customFee(feeSendState: FeeSelectorState, modifier: M
internal fun LazyListScope.subtractButton(
receivedAmount: String,
isSubtract: Boolean,
isSubtractAvailable: Boolean,
clickIntents: SendClickIntents,
modifier: Modifier = Modifier,
) {
item {
SendSpeedSubtract(
receivingAmount = receivedAmount,
isSubtract = isSubtract,
onSelectClick = clickIntents::onSubtractSelect,
modifier = modifier
.padding(vertical = TangemTheme.dimens.spacing12)
.animateItemPlacement(),
)
if (isSubtractAvailable) {
item {
SendSpeedSubtract(
receivingAmount = receivedAmount,
isSubtract = isSubtract,
onSelectClick = clickIntents::onSubtractSelect,
modifier = modifier
.padding(vertical = TangemTheme.dimens.spacing12)
.animateItemPlacement(),
)
}
}
}

View file

@ -17,10 +17,7 @@ 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.*
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.utils.convertToAmount
@ -40,10 +37,7 @@ import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
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.fee.FeeSelectorState
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.features.send.impl.presentation.state.fee.*
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
import dagger.hilt.android.lifecycle.HiltViewModel
@ -73,8 +67,10 @@ internal class SendViewModel @Inject constructor(
private val parseSharedAddressUseCase: ParseSharedAddressUseCase,
private val walletManagersFacade: WalletManagersFacade,
private val reduxStateHolder: ReduxStateHolder,
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
validateWalletMemoUseCase: ValidateWalletMemoUseCase,
getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, SendClickIntents {
@ -106,7 +102,6 @@ internal class SendViewModel @Inject constructor(
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
userWalletProvider = Provider { userWallet },
)
private val eventStateFactory = SendEventStateFactory(
@ -115,6 +110,15 @@ internal class SendViewModel @Inject constructor(
feeStateFactory = feeStateFactory,
)
private val feeNotificationFactory = FeeNotificationFactory(
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
currentStateProvider = Provider { uiState },
userWalletProvider = Provider { userWallet },
clickIntents = this,
getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase,
)
private val sendNotificationFactory = SendNotificationFactory(
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
@ -129,6 +133,7 @@ internal class SendViewModel @Inject constructor(
private set
private var userWallet: UserWallet by Delegates.notNull()
private var isAmountSubtractAvailable: Boolean = false
private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
@ -137,6 +142,7 @@ internal class SendViewModel @Inject constructor(
private var feeJobHolder = JobHolder()
private var addressValidationJobHolder = JobHolder()
private var sendNotificationsJobHolder = JobHolder()
private var feeNotificationsJobHolder = JobHolder()
private var qrScannerJobHolder = JobHolder()
private var checkFeeUpdateScheduler = SingleTaskScheduler<Either<GetFeeError, TransactionFee>?>()
@ -163,6 +169,7 @@ internal class SendViewModel @Inject constructor(
getUserWalletUseCase(userWalletId).fold(
ifRight = { wallet ->
userWallet = wallet
checkIfSubtractAvailable()
getCurrenciesStatusUpdates(owner, wallet)
},
ifLeft = {
@ -326,6 +333,16 @@ internal class SendViewModel @Inject constructor(
.saveIn(sendNotificationsJobHolder)
}
private fun updateFeeNotifications() {
feeNotificationFactory.create()
.conflate()
.distinctUntilChanged()
.onEach { uiState = feeStateFactory.getFeeNotificationState(notifications = it) }
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(feeNotificationsJobHolder)
}
// region screen state navigation
override fun popBackStack() = stateRouter.popBackStack()
override fun onBackClick() = stateRouter.onBackClick()
@ -429,30 +446,41 @@ internal class SendViewModel @Inject constructor(
override fun onFeeSelectorClick(feeType: FeeType) {
uiState = feeStateFactory.onFeeSelectedState(feeType)
updateFeeNotifications()
}
override fun onCustomFeeValueChange(index: Int, value: String) {
uiState = feeStateFactory.onCustomFeeValueChange(index, value)
updateFeeNotifications()
}
override fun onSubtractSelect(value: Boolean) {
uiState = feeStateFactory.onSubtractSelect(value)
updateFeeNotifications()
}
private fun loadFee() {
viewModelScope.launch(dispatchers.main) {
uiState = feeStateFactory.onFeeOnLoadingState()
uiState = callFeeUseCase()?.fold(
ifRight = {
feeStateFactory.onFeeOnLoadedState(it)
ifRight = { fees ->
feeStateFactory.onFeeOnLoadedState(fees, isAmountSubtractAvailable)
},
ifLeft = {
feeStateFactory.onFeeOnErrorState()
},
) ?: feeStateFactory.onFeeOnErrorState()
updateFeeNotifications()
}.saveIn(feeJobHolder)
}
private suspend fun checkIfSubtractAvailable() {
isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, cryptoCurrency).fold(
ifRight = { it },
ifLeft = { false },
)
}
private suspend fun callFeeUseCase(): Either<GetFeeError, TransactionFee>? {
val amountState = uiState.amountState ?: return null
val recipientState = uiState.recipientState ?: return null
@ -501,8 +529,11 @@ internal class SendViewModel @Inject constructor(
val memo = uiState.recipientState?.memoTextField?.value
val fee = feeSelectorState.getFee()
val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return
val amountToSend = if (feeState.isSubtract) feeState.receivedAmountValue else amountValue
val amountToSend = if (feeState.isSubtract && isAmountSubtractAvailable) {
feeState.receivedAmountValue
} else {
amountValue
}
viewModelScope.launch(dispatchers.main) {
createTransactionUseCase(