Updated on 2026-08-14
This commit is contained in:
parent
885db20951
commit
cd61d9c841
9 changed files with 369 additions and 143 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue