Updated on 2026-08-14
This commit is contained in:
commit
8da4dc8205
23 changed files with 626 additions and 228 deletions
|
|
@ -98,6 +98,7 @@
|
|||
<string name="common_fee_selector_option_slow">Медленно</string>
|
||||
<string name="common_fee_selector_title">Скорость и комиссия</string>
|
||||
<string name="common_generate_addresses">Получить адреса</string>
|
||||
<string name="common_go_to_provider">К провайдеру</string>
|
||||
<string name="common_import">Импортировать</string>
|
||||
<string name="common_like">Нравится</string>
|
||||
<string name="common_locked">Заблокирован</string>
|
||||
|
|
@ -449,6 +450,8 @@
|
|||
<string name="send_notification_exceed_fee_title">Комиссия превышает баланс</string>
|
||||
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.</string>
|
||||
<string name="send_notification_high_fee_title">Увеличение комиссии</string>
|
||||
<string name="send_notification_fee_too_high_title">Установлена высокая комиссия</string>
|
||||
<string name="send_notification_fee_too_high_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
|
||||
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению</string>
|
||||
<string name="send_notification_invalid_amount_title">Недопустимая сумма</string>
|
||||
<string name="send_notification_invalid_minimum_amount_text">Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s.</string>
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@
|
|||
<string name="common_fee_selector_option_market">Market</string>
|
||||
<string name="common_fee_selector_option_slow">Slow</string>
|
||||
<string name="common_fee_selector_title">Speed and fee</string>
|
||||
<string name="common_go_to_provider">Go to provider</string>
|
||||
<string name="common_generate_addresses">Get addresses</string>
|
||||
<string name="common_import">Import</string>
|
||||
<string name="common_learn_and_earn">Learn & Earn</string>
|
||||
|
|
@ -223,7 +224,6 @@
|
|||
<string name="express_exchange_token_list_subtitle">List of all tokens added to your wallet</string>
|
||||
<string name="express_fetch_best_rates">Fetching best rates...</string>
|
||||
<string name="express_floating_rate">Floating rate</string>
|
||||
<string name="express_go_to_provider">Go to provider</string>
|
||||
<string name="express_provider">Provider</string>
|
||||
<string name="express_provider_best_rate">Best rate</string>
|
||||
<string name="express_provider_min_amount">Available from %s</string>
|
||||
|
|
@ -473,6 +473,8 @@
|
|||
<string name="send_notification_exceed_fee_title">Fee exceeds balance</string>
|
||||
<string name="send_notification_high_fee_text">The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01.</string>
|
||||
<string name="send_notification_high_fee_title">Fee is increased</string>
|
||||
<string name="send_notification_fee_too_high_title">Custom fee is high</string>
|
||||
<string name="send_notification_fee_too_high_text">The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.</string>
|
||||
<string name="send_notification_invalid_amount_text">The included commission exceeds the transfer amount, leading to a negative value</string>
|
||||
<string name="send_notification_invalid_amount_title">Invalid amount</string>
|
||||
<string name="send_notification_invalid_minimum_amount_text">The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s.</string>
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ class DefaultWalletAddressServiceRepository(
|
|||
) : WalletAddressServiceRepository {
|
||||
override suspend fun validate(userWalletId: UserWalletId, network: Network, address: String): Boolean {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return false
|
||||
|
||||
return if (blockchain.isNear()) {
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return false
|
||||
(walletManager as? NearWalletManager)?.validateAddress(address) ?: false
|
||||
} else {
|
||||
blockchain.validateAddress(address)
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ dependencies {
|
|||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.send.api)
|
||||
implementation(projects.features.tokendetails.api)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
package com.tangem.features.send.impl.navigation
|
||||
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.send.impl.presentation.SendFragment
|
||||
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
|
||||
|
||||
internal class DefaultSendRouter(
|
||||
private val reduxNavController: ReduxNavController,
|
||||
|
|
@ -14,4 +19,16 @@ internal class DefaultSendRouter(
|
|||
override fun openUrl(url: String) {
|
||||
reduxNavController.navigate(NavigationAction.OpenUrl(url = url))
|
||||
}
|
||||
|
||||
override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) {
|
||||
reduxNavController.navigate(
|
||||
action = NavigationAction.NavigateTo(
|
||||
screen = AppScreen.WalletDetails,
|
||||
bundle = bundleOf(
|
||||
TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue,
|
||||
TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,14 @@
|
|||
package com.tangem.features.send.impl.navigation
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
|
||||
interface InnerSendRouter : SendRouter {
|
||||
|
||||
/** Open website by [url] */
|
||||
fun openUrl(url: String)
|
||||
|
||||
/** Open token details screen by [userWalletId] and [currency] */
|
||||
fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.blockchain.common.address.Address
|
|||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.txhistory.models.TxHistoryItem
|
||||
|
|
@ -12,21 +13,23 @@ import com.tangem.domain.wallets.models.UserWallet
|
|||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeCustomFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.*
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.calculateReceiveAmount
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.isNotAddressInWallet
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.validateMemo
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SendStateFactory(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
|
|
@ -34,6 +37,7 @@ internal class SendStateFactory(
|
|||
private val walletAddressesProvider: Provider<Set<Address>>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) {
|
||||
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
|
@ -47,6 +51,12 @@ internal class SendStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
private val feeNotificationFactory = FeeNotificationFactory(
|
||||
coinCryptoCurrencyStatusProvider = coinCryptoCurrencyStatusProvider,
|
||||
userWalletProvider = userWalletProvider,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
private val amountStateConverter by lazy {
|
||||
SendAmountStateConverter(
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
|
|
@ -196,28 +206,128 @@ internal class SendStateFactory(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeOnLoadingState() {
|
||||
currentStateProvider().feeState?.feeSelectorState?.update {
|
||||
FeeSelectorState.Loading
|
||||
}
|
||||
}
|
||||
|
||||
fun onFeeOnLoadedState(fees: TransactionFee) {
|
||||
currentStateProvider().feeState?.feeSelectorState?.update {
|
||||
FeeSelectorState.Content(
|
||||
fees = fees,
|
||||
customValues = customFeeFieldConverter.convert(fees.normal),
|
||||
)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region fee
|
||||
fun onFeeSelectedState(feeType: FeeType) {
|
||||
currentStateProvider().feeState?.feeSelectorState?.update {
|
||||
(it as? FeeSelectorState.Content)?.copy(selectedFee = feeType) ?: it
|
||||
fun onFeeOnLoadingState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = FeeSelectorState.Loading
|
||||
return state.copy(
|
||||
feeState = feeState.copy(
|
||||
feeSelectorState = feeSelectorState,
|
||||
notifications = persistentListOf(),
|
||||
isPrimaryButtonEnabled = feeSelectorState.isPrimaryButtonEnabled(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeOnLoadedState(fees: TransactionFee, isMaxAmount: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = FeeSelectorState.Content(
|
||||
fees = fees,
|
||||
customValues = customFeeFieldConverter.convert(fees.normal),
|
||||
)
|
||||
val updatedState = feeState.copy(
|
||||
feeSelectorState = feeSelectorState,
|
||||
receivedAmount = feeSelectorState.updateReceiveAmount(),
|
||||
isSubtract = isMaxAmount,
|
||||
)
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(feeState = updatedState),
|
||||
isPrimaryButtonEnabled = feeSelectorState.isPrimaryButtonEnabled(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeSelectedState(feeType: FeeType): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType)
|
||||
val updatedState = feeState.copy(
|
||||
receivedAmount = updatedFeeSelectorState.updateReceiveAmount(),
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
isPrimaryButtonEnabled = updatedFeeSelectorState.isPrimaryButtonEnabled(),
|
||||
)
|
||||
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(feeState = updatedState),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onCustomFeeValueChange(index: Int, value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val updatedFeeSelectorState = feeSelectorState.copy(
|
||||
customValues = feeSelectorState.customValues.toMutableList().apply {
|
||||
set(index, feeSelectorState.customValues[index].copy(value = value))
|
||||
}.toImmutableList(),
|
||||
)
|
||||
val updatedState = feeState.copy(
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
receivedAmount = updatedFeeSelectorState.updateReceiveAmount(),
|
||||
isPrimaryButtonEnabled = updatedFeeSelectorState.isPrimaryButtonEnabled(),
|
||||
)
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(feeState = updatedState),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onSubtractSelect(value: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val updatedState = feeState.copy(
|
||||
isSubtract = value,
|
||||
receivedAmount = if (value) {
|
||||
feeSelectorState.updateReceiveAmount()
|
||||
} else {
|
||||
feeState.receivedAmount
|
||||
},
|
||||
)
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(feeState = updatedState),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun FeeSelectorState.isPrimaryButtonEnabled(): Boolean {
|
||||
return when (this) {
|
||||
is FeeSelectorState.Loading -> false
|
||||
is FeeSelectorState.Content -> {
|
||||
val choosableFee = fees as? TransactionFee.Choosable
|
||||
val customValue = customValues.firstOrNull()?.value?.toBigDecimalOrNull()
|
||||
val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
val fee = when (selectedFee) {
|
||||
FeeType.SLOW -> choosableFee?.minimum?.amount?.value
|
||||
FeeType.MARKET -> fees.normal.amount.value
|
||||
FeeType.FAST -> choosableFee?.priority?.amount?.value
|
||||
FeeType.CUSTOM -> customValue
|
||||
} ?: BigDecimal.ZERO
|
||||
|
||||
val isNotEmptyCustom = !customValue.isNullOrZero() && selectedFee == FeeType.CUSTOM
|
||||
val isNotCustom = selectedFee != FeeType.CUSTOM
|
||||
fee < balance && (isNotEmptyCustom || isNotCustom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun FeeSelectorState.Content.updateReceiveAmount(): String {
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
return BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = calculateReceiveAmount(currentStateProvider()),
|
||||
cryptoCurrency = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
|
|
@ -9,9 +9,12 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
/**
|
||||
|
|
@ -62,9 +65,11 @@ internal sealed class SendStates {
|
|||
data class FeeState(
|
||||
override val type: SendUiStateType = SendUiStateType.Fee,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val feeSelectorState: MutableStateFlow<FeeSelectorState> = MutableStateFlow(FeeSelectorState.Empty),
|
||||
val isSubtract: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
val receivedAmount: MutableStateFlow<String> = MutableStateFlow(""),
|
||||
val feeSelectorState: FeeSelectorState = FeeSelectorState.Loading,
|
||||
val isSubtract: Boolean = false,
|
||||
val receivedAmount: String = "",
|
||||
val notifications: ImmutableList<SendFeeNotification> = persistentListOf(),
|
||||
val isPrimaryButtonEnabled: Boolean = false,
|
||||
) : SendStates()
|
||||
|
||||
/** Send state */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Calculate receiving amount when fee is subtracted from sending amount
|
||||
*/
|
||||
internal fun FeeSelectorState.Content.calculateReceiveAmount(uiState: SendUiState): BigDecimal {
|
||||
val amount = uiState.amountState?.amountTextField?.value ?: return BigDecimal.ZERO
|
||||
|
||||
val fee = when (fees) {
|
||||
is TransactionFee.Choosable -> {
|
||||
when (selectedFee) {
|
||||
FeeType.SLOW -> fees.minimum.amount.value
|
||||
FeeType.MARKET -> fees.normal.amount.value
|
||||
FeeType.FAST -> fees.priority.amount.value
|
||||
FeeType.CUSTOM -> customValues.firstOrNull()?.value?.let { BigDecimal(it.ifEmpty { "0" }) }
|
||||
}
|
||||
}
|
||||
is TransactionFee.Single -> fees.normal.amount.value
|
||||
} ?: BigDecimal.ZERO
|
||||
|
||||
return BigDecimal(amount).minus(fee)
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.extensions.networkIconResId
|
||||
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.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 java.math.BigDecimal
|
||||
|
||||
internal class FeeNotificationFactory(
|
||||
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
) {
|
||||
|
||||
operator fun invoke(feeState: SendStates.FeeState): ImmutableList<SendFeeNotification> {
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return persistentListOf()
|
||||
val customFee = feeSelectorState.customValues
|
||||
val selectedFee = feeSelectorState.selectedFee
|
||||
|
||||
return buildList {
|
||||
addTooLowNotification(feeSelectorState.fees, selectedFee, customFee)
|
||||
addTooHighNotification(feeSelectorState.fees, selectedFee, customFee)
|
||||
addFeeCoverageNotification()
|
||||
addExceedsBalanceNotification(feeSelectorState)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun MutableList<SendFeeNotification>.addTooLowNotification(
|
||||
transactionFee: TransactionFee,
|
||||
selectedFee: FeeType,
|
||||
customFee: List<SendTextField.CustomFee>,
|
||||
) {
|
||||
val multipleFees = transactionFee as? TransactionFee.Choosable ?: return
|
||||
val minimumValue = multipleFees.minimum.amount.value ?: return
|
||||
val customValue = customFee.firstOrNull()?.value?.toBigDecimalOrNull() ?: return
|
||||
if (selectedFee == FeeType.CUSTOM && minimumValue > customValue) {
|
||||
add(SendFeeNotification.Informational.TooLow)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendFeeNotification>.addTooHighNotification(
|
||||
transactionFee: TransactionFee,
|
||||
selectedFee: FeeType,
|
||||
customFee: List<SendTextField.CustomFee>,
|
||||
) {
|
||||
val multipleFees = transactionFee as? TransactionFee.Choosable ?: return
|
||||
val highValue = multipleFees.priority.amount.value ?: return
|
||||
val customValue = customFee.firstOrNull()?.value?.toBigDecimalOrNull() ?: return
|
||||
val diff = customValue / highValue
|
||||
if (selectedFee == FeeType.CUSTOM && diff > FEE_MAX_DIFF) {
|
||||
add(SendFeeNotification.Warning.TooHigh(diff.toInt().toString()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendFeeNotification>.addFeeCoverageNotification() {
|
||||
// TODO add fee coverage condition [REDACTED_JIRA]
|
||||
add(SendFeeNotification.Warning.NetworkCoverage)
|
||||
}
|
||||
|
||||
private fun MutableList<SendFeeNotification>.addExceedsBalanceNotification(
|
||||
feeSelectorState: FeeSelectorState.Content,
|
||||
) {
|
||||
val coinCryptoCurrency = coinCryptoCurrencyStatusProvider()
|
||||
val choosableFee = feeSelectorState.fees as? TransactionFee.Choosable
|
||||
val fee = when (feeSelectorState.selectedFee) {
|
||||
FeeType.SLOW -> choosableFee?.minimum?.amount?.value
|
||||
FeeType.MARKET -> feeSelectorState.fees.normal.amount.value
|
||||
FeeType.FAST -> choosableFee?.priority?.amount?.value
|
||||
FeeType.CUSTOM -> feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull()
|
||||
} ?: return
|
||||
|
||||
if (fee > coinCryptoCurrency.value.amount) {
|
||||
add(
|
||||
SendFeeNotification.Error.ExceedsBalance(
|
||||
coinCryptoCurrency.currency.networkIconResId,
|
||||
) {
|
||||
clickIntents.onTokenDetailsClick(
|
||||
userWalletProvider().walletId,
|
||||
coinCryptoCurrency.currency,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val FEE_MAX_DIFF = BigDecimal(5)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,19 +3,18 @@ package com.tangem.features.send.impl.presentation.state.fee
|
|||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Immutable
|
||||
internal sealed class FeeSelectorState {
|
||||
|
||||
object Loading : FeeSelectorState()
|
||||
|
||||
object Empty : FeeSelectorState()
|
||||
|
||||
data class Content(
|
||||
val fees: TransactionFee,
|
||||
val selectedFee: FeeType = FeeType.MARKET,
|
||||
val customValues: MutableStateFlow<List<SendTextField.CustomFee>> = MutableStateFlow(emptyList()),
|
||||
val customValues: ImmutableList<SendTextField.CustomFee> = persistentListOf(),
|
||||
) : FeeSelectorState()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,15 +11,16 @@ 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 com.tangem.utils.converter.Converter
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SendFeeCustomFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
) : Converter<Fee, MutableStateFlow<List<SendTextField.CustomFee>>> {
|
||||
) : Converter<Fee, ImmutableList<SendTextField.CustomFee>> {
|
||||
|
||||
override fun convert(value: Fee): MutableStateFlow<List<SendTextField.CustomFee>> {
|
||||
val ethereumFee = value as? Fee.Ethereum ?: return MutableStateFlow(emptyList())
|
||||
override fun convert(value: Fee): ImmutableList<SendTextField.CustomFee> {
|
||||
val ethereumFee = value as? Fee.Ethereum ?: return persistentListOf()
|
||||
val appCurrency = appCurrencyProvider()
|
||||
|
||||
val maxFeeFiat = BigDecimalFormatter.formatFiatAmount(
|
||||
|
|
@ -28,32 +29,30 @@ internal class SendFeeCustomFieldConverter(
|
|||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
|
||||
return MutableStateFlow(
|
||||
listOf(
|
||||
SendTextField.CustomFee(
|
||||
value = ethereumFee.amount.value.toString(),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(0, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
label = TextReference.Str(maxFeeFiat),
|
||||
return persistentListOf(
|
||||
SendTextField.CustomFee(
|
||||
value = ethereumFee.amount.value.toString(),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(0, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
SendTextField.CustomFee(
|
||||
value = ethereumFee.gasPrice.toString(),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(1, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
label = TextReference.Str(maxFeeFiat),
|
||||
),
|
||||
SendTextField.CustomFee(
|
||||
value = ethereumFee.gasPrice.toString(),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(1, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
SendTextField.CustomFee(
|
||||
value = ethereumFee.gasLimit.toString(),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(2, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
SendTextField.CustomFee(
|
||||
value = ethereumFee.gasLimit.toString(),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(2, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.send.impl.R
|
||||
|
||||
@Immutable
|
||||
sealed class SendFeeNotification(val config: NotificationConfig) {
|
||||
|
||||
sealed class Informational(
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
) : SendFeeNotification(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
),
|
||||
) {
|
||||
object TooLow : Informational(
|
||||
title = resourceReference(id = R.string.send_notification_transaction_delay_title),
|
||||
subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Warning(
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
) : SendFeeNotification(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
),
|
||||
) {
|
||||
data class TooHigh(
|
||||
val value: String,
|
||||
) : Warning(
|
||||
title = resourceReference(id = R.string.send_notification_fee_too_high_title),
|
||||
subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)),
|
||||
)
|
||||
|
||||
object NetworkCoverage : Warning(
|
||||
title = resourceReference(id = R.string.send_network_fee_warning_title),
|
||||
subtitle = resourceReference(id = R.string.send_network_fee_warning_content),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Error(
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
val iconResId: Int,
|
||||
val buttonsState: NotificationConfig.ButtonsState,
|
||||
) : SendFeeNotification(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = iconResId,
|
||||
buttonsState = buttonsState,
|
||||
),
|
||||
) {
|
||||
data class ExceedsBalance(
|
||||
val networkIconId: Int,
|
||||
val onClick: () -> Unit,
|
||||
) : Error(
|
||||
title = resourceReference(id = R.string.send_notification_exceed_fee_title),
|
||||
subtitle = resourceReference(id = R.string.send_notification_exceed_fee_text),
|
||||
iconResId = networkIconId,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(R.string.common_go_to_provider),
|
||||
onClick = onClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.material3.Icon
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
|
|
@ -76,16 +77,19 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier
|
|||
val isSending = uiState.sendState?.isSending?.collectAsStateWithLifecycle()?.value ?: false
|
||||
val txUrl = uiState.sendState?.txUrl?.collectAsStateWithLifecycle()?.value.orEmpty()
|
||||
|
||||
val (buttonTextId, buttonClick) = getButtonData(
|
||||
currentState = currentState,
|
||||
isSuccess = isSuccess,
|
||||
uiState = uiState,
|
||||
)
|
||||
val (buttonTextId, buttonClick) = remember {
|
||||
getButtonData(
|
||||
currentState = currentState,
|
||||
isSuccess = isSuccess,
|
||||
uiState = uiState,
|
||||
)
|
||||
}
|
||||
|
||||
val isButtonEnabled = when (currentState.value) {
|
||||
SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false
|
||||
SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled ?: false
|
||||
else -> true
|
||||
val isButtonEnabled = remember {
|
||||
isButtonEnabled(
|
||||
currentState = currentState,
|
||||
uiState = uiState,
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedContent(
|
||||
|
|
@ -181,4 +185,13 @@ private fun getButtonData(
|
|||
R.string.common_send
|
||||
} to uiState.clickIntents::onSendClick
|
||||
}
|
||||
}
|
||||
|
||||
private fun isButtonEnabled(currentState: State<SendUiStateType>, uiState: SendUiState): Boolean {
|
||||
return when (currentState.value) {
|
||||
SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false
|
||||
SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled ?: false
|
||||
SendUiStateType.Fee -> uiState.feeState?.isPrimaryButtonEnabled ?: false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
|
|
@ -16,20 +15,21 @@ import com.tangem.features.send.impl.R
|
|||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
private const val ETHEREUM_UNIT = "GWEI"
|
||||
|
||||
@Composable
|
||||
internal fun SendCustomFeeEthereum(
|
||||
customValues: State<List<SendTextField.CustomFee>>,
|
||||
customValues: ImmutableList<SendTextField.CustomFee>,
|
||||
selectedFee: FeeType,
|
||||
symbol: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (selectedFee == FeeType.CUSTOM && customValues.value.isNotEmpty()) {
|
||||
val fee = customValues.value[0]
|
||||
val gasPrice = customValues.value[1]
|
||||
val gasLimit = customValues.value[2]
|
||||
if (selectedFee == FeeType.CUSTOM && customValues.isNotEmpty()) {
|
||||
val fee = customValues[0]
|
||||
val gasPrice = customValues[1]
|
||||
val gasLimit = customValues[2]
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
|
|
|
|||
|
|
@ -3,25 +3,32 @@ package com.tangem.features.send.impl.presentation.ui.fee
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
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
|
||||
val feeSendState = state.feeSelectorState.collectAsStateWithLifecycle()
|
||||
val feeSendState = state.feeSelectorState
|
||||
val notifications = state.notifications
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -39,42 +46,90 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S
|
|||
clickIntents = clickIntents,
|
||||
)
|
||||
}
|
||||
if (feeSendState.value is FeeSelectorState.Content) {
|
||||
item(
|
||||
key = FEE_CUSTOM_KEY,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = feeSendState.value is FeeSelectorState.Content,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
val fee = feeSendState.value as FeeSelectorState.Content
|
||||
val customValues = fee.customValues.collectAsStateWithLifecycle()
|
||||
SendCustomFeeEthereum(
|
||||
customValues = customValues,
|
||||
selectedFee = fee.selectedFee,
|
||||
symbol = state.cryptoCurrencyStatus.currency.symbol,
|
||||
modifier = Modifier
|
||||
.animateItemPlacement(),
|
||||
)
|
||||
}
|
||||
notifications(notifications)
|
||||
customFee(
|
||||
feeSendState = feeSendState,
|
||||
cryptoCurrencySymbol = state.cryptoCurrencyStatus.currency.symbol,
|
||||
)
|
||||
subtractButton(
|
||||
feeSendState = feeSendState,
|
||||
receivedAmount = state.receivedAmount,
|
||||
isSubtract = state.isSubtract,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun LazyListScope.notifications(configs: ImmutableList<SendFeeNotification>, modifier: Modifier = Modifier) {
|
||||
items(
|
||||
items = configs,
|
||||
key = { it::class.java },
|
||||
contentType = { it::class.java },
|
||||
itemContent = {
|
||||
Notification(
|
||||
config = it.config,
|
||||
modifier = modifier.animateItemPlacement(),
|
||||
containerColor = TangemTheme.colors.button.disabled,
|
||||
iconTint = when (it) {
|
||||
is SendFeeNotification.Informational -> TangemTheme.colors.icon.accent
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun LazyListScope.customFee(
|
||||
feeSendState: FeeSelectorState,
|
||||
cryptoCurrencySymbol: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
item(
|
||||
key = FEE_CUSTOM_KEY,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = feeSendState is FeeSelectorState.Content,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.animateItemPlacement()
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
(feeSendState as? FeeSelectorState.Content)?.let { fee ->
|
||||
val customValues = fee.customValues
|
||||
SendCustomFeeEthereum(
|
||||
customValues = customValues,
|
||||
selectedFee = fee.selectedFee,
|
||||
symbol = cryptoCurrencySymbol,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun LazyListScope.subtractButton(
|
||||
feeSendState: FeeSelectorState,
|
||||
receivedAmount: String,
|
||||
isSubtract: Boolean,
|
||||
clickIntents: SendClickIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
(feeSendState as? FeeSelectorState.Content)?.let { state ->
|
||||
item {
|
||||
val topPadding = (feeSendState.value as? FeeSelectorState.Content)?.let { state ->
|
||||
if (state.selectedFee != FeeType.CUSTOM) {
|
||||
TangemTheme.dimens.spacing8
|
||||
} else {
|
||||
TangemTheme.dimens.spacing0
|
||||
}
|
||||
} ?: TangemTheme.dimens.spacing0
|
||||
val selectedFeeValue = state.selectedFee
|
||||
val topPadding = if (selectedFeeValue != FeeType.CUSTOM) {
|
||||
TangemTheme.dimens.spacing8
|
||||
} else {
|
||||
TangemTheme.dimens.spacing0
|
||||
}
|
||||
|
||||
SendSpeedSubtract(
|
||||
receivingAmount = state.receivedAmount,
|
||||
isSubtract = state.isSubtract,
|
||||
receivingAmount = receivedAmount,
|
||||
isSubtract = isSubtract,
|
||||
onSelectClick = clickIntents::onSubtractSelect,
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.animateItemPlacement()
|
||||
.padding(
|
||||
top = topPadding,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -36,7 +35,7 @@ import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
|||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun SendSpeedSelector(
|
||||
state: State<FeeSelectorState>,
|
||||
state: FeeSelectorState,
|
||||
clickIntents: SendClickIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -50,47 +49,48 @@ internal fun SendSpeedSelector(
|
|||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
when (val selector = state.value) {
|
||||
when (state) {
|
||||
FeeSelectorState.Loading -> {
|
||||
SendSpeedSelectorItemLoading()
|
||||
SendSpeedSelectorItemLoading()
|
||||
SendSpeedSelectorItemLoading()
|
||||
}
|
||||
is FeeSelectorState.Content -> {
|
||||
when (selector.fees) {
|
||||
when (state.fees) {
|
||||
is TransactionFee.Choosable -> {
|
||||
val isSelected = state.selectedFee
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_slow,
|
||||
iconRes = R.drawable.ic_tortoise_24,
|
||||
amount = TextReference.Str(selector.fees.minimum.amount.value.toString()),
|
||||
symbol = TextReference.Str(selector.fees.minimum.amount.currencySymbol),
|
||||
isSelected = selector.selectedFee == FeeType.SLOW,
|
||||
amount = TextReference.Str(state.fees.minimum.amount.value.toString()),
|
||||
symbol = TextReference.Str(state.fees.minimum.amount.currencySymbol),
|
||||
isSelected = isSelected == FeeType.SLOW,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.SLOW) },
|
||||
)
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_market,
|
||||
iconRes = R.drawable.ic_bird_24,
|
||||
amount = TextReference.Str(selector.fees.normal.amount.value.toString()),
|
||||
symbol = TextReference.Str(selector.fees.normal.amount.currencySymbol),
|
||||
isSelected = selector.selectedFee == FeeType.MARKET,
|
||||
amount = TextReference.Str(state.fees.normal.amount.value.toString()),
|
||||
symbol = TextReference.Str(state.fees.normal.amount.currencySymbol),
|
||||
isSelected = isSelected == FeeType.MARKET,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) },
|
||||
)
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_fast,
|
||||
iconRes = R.drawable.ic_hare_24,
|
||||
amount = TextReference.Str(selector.fees.priority.amount.value.toString()),
|
||||
symbol = TextReference.Str(selector.fees.priority.amount.currencySymbol),
|
||||
isSelected = selector.selectedFee == FeeType.FAST,
|
||||
amount = TextReference.Str(state.fees.priority.amount.value.toString()),
|
||||
symbol = TextReference.Str(state.fees.priority.amount.currencySymbol),
|
||||
isSelected = isSelected == FeeType.FAST,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.FAST) },
|
||||
showDivider = selector.fees.normal is Fee.Ethereum,
|
||||
showDivider = state.fees.normal is Fee.Ethereum,
|
||||
)
|
||||
if (selector.fees.normal is Fee.Ethereum) {
|
||||
if (state.fees.normal is Fee.Ethereum) {
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_custom,
|
||||
iconRes = R.drawable.ic_edit_24,
|
||||
isSelected = selector.selectedFee == FeeType.CUSTOM,
|
||||
isSelected = isSelected == FeeType.CUSTOM,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.CUSTOM) },
|
||||
showDivider = selector.fees.normal !is Fee.Ethereum,
|
||||
showDivider = state.fees.normal !is Fee.Ethereum,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -99,15 +99,14 @@ internal fun SendSpeedSelector(
|
|||
titleRes = R.string.common_fee_selector_option_market,
|
||||
iconRes = R.drawable.ic_bird_24,
|
||||
isSelected = true,
|
||||
amount = TextReference.Str(selector.fees.normal.amount.value.toString()),
|
||||
symbol = TextReference.Str(selector.fees.normal.amount.currencySymbol),
|
||||
amount = TextReference.Str(state.fees.normal.amount.value.toString()),
|
||||
symbol = TextReference.Str(state.fees.normal.amount.currencySymbol),
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) },
|
||||
showDivider = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
FeeSelectorState.Empty -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,25 +11,20 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.TangemSwitch
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Composable
|
||||
internal fun SendSpeedSubtract(
|
||||
receivingAmount: StateFlow<String>,
|
||||
isSubtract: StateFlow<Boolean>,
|
||||
receivingAmount: String,
|
||||
isSubtract: Boolean,
|
||||
onSelectClick: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isSelected = isSubtract.collectAsStateWithLifecycle()
|
||||
val footer = receivingAmount.collectAsStateWithLifecycle()
|
||||
|
||||
val footerText = if (isSelected.value) {
|
||||
stringResource(R.string.send_amount_substract_footer, footer.value)
|
||||
val footerText = if (isSubtract) {
|
||||
stringResource(R.string.send_amount_substract_footer, receivingAmount)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -58,7 +53,7 @@ internal fun SendSpeedSubtract(
|
|||
.padding(end = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
TangemSwitch(
|
||||
checked = isSelected.value,
|
||||
checked = isSubtract,
|
||||
onCheckedChange = onSelectClick,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,18 +166,18 @@ private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess:
|
|||
|
||||
@Composable
|
||||
private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: State<Boolean>, onClick: () -> Unit) {
|
||||
val feeSelector =
|
||||
feeState.feeSelectorState.collectAsStateWithLifecycle().value as? FeeSelectorState.Content ?: return
|
||||
val customValue = feeSelector.customValues.collectAsStateWithLifecycle().value.getOrNull(0)
|
||||
val feeSelector = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
|
||||
val customValue = feeSelector.customValues.getOrNull(0)
|
||||
val selectedFee = feeSelector.selectedFee
|
||||
|
||||
val feeValue = formatCryptoAmount(
|
||||
cryptoCurrency = feeState.cryptoCurrencyStatus.currency,
|
||||
cryptoAmount = when (val selectedFee = feeSelector.fees) {
|
||||
is TransactionFee.Single -> selectedFee.normal.amount.value
|
||||
is TransactionFee.Choosable -> when (feeSelector.selectedFee) {
|
||||
FeeType.SLOW -> selectedFee.minimum.amount.value
|
||||
FeeType.MARKET -> selectedFee.normal.amount.value
|
||||
FeeType.FAST -> selectedFee.priority.amount.value
|
||||
cryptoAmount = when (val fees = feeSelector.fees) {
|
||||
is TransactionFee.Single -> fees.normal.amount.value
|
||||
is TransactionFee.Choosable -> when (selectedFee) {
|
||||
FeeType.SLOW -> fees.minimum.amount.value
|
||||
FeeType.MARKET -> fees.normal.amount.value
|
||||
FeeType.FAST -> fees.priority.amount.value
|
||||
FeeType.CUSTOM -> customValue?.value.toBigDecimalOrDefault()
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.features.send.impl.presentation.viewmodel
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
|
||||
interface SendClickIntents {
|
||||
|
|
@ -14,6 +16,8 @@ interface SendClickIntents {
|
|||
|
||||
fun onQrCodeScanClick()
|
||||
|
||||
fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency)
|
||||
|
||||
// region Amount
|
||||
fun onAmountValueChange(value: String)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ import com.tangem.blockchain.common.TransactionExtras
|
|||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
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
|
||||
|
|
@ -67,6 +67,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
|
||||
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
|
||||
|
|
@ -99,12 +100,14 @@ internal class SendViewModel @Inject constructor(
|
|||
walletAddressesProvider = Provider { walletAddresses },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||
)
|
||||
|
||||
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
|
||||
private set
|
||||
|
||||
private var userWallet: UserWallet by Delegates.notNull()
|
||||
private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
private var walletAddresses = emptySet<Address>()
|
||||
|
||||
|
|
@ -131,7 +134,7 @@ internal class SendViewModel @Inject constructor(
|
|||
getUserWalletUseCase(userWalletId).fold(
|
||||
ifRight = { wallet ->
|
||||
userWallet = wallet
|
||||
getCurrencyStatusUpdates(owner, wallet)
|
||||
getCurrenciesStatusUpdates(owner, wallet)
|
||||
},
|
||||
ifLeft = {
|
||||
// TODO add error handling
|
||||
|
|
@ -141,28 +144,54 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getCurrencyStatusUpdates(owner: LifecycleOwner, wallet: UserWallet) {
|
||||
private fun getCurrenciesStatusUpdates(owner: LifecycleOwner, wallet: UserWallet) {
|
||||
val isSingleWallet = wallet.scanResponse.walletData?.token != null && !wallet.isMultiCurrency
|
||||
getCurrencyStatusUpdatesUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
isSingleWalletWithTokens = isSingleWallet,
|
||||
)
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { either ->
|
||||
either.onRight {
|
||||
cryptoCurrencyStatus = it
|
||||
|
||||
if (cryptoCurrency is CryptoCurrency.Coin) {
|
||||
getCurrencyStatusUpdates(isSingleWallet = isSingleWallet)
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.onEach { currencyStatus ->
|
||||
currencyStatus.onRight {
|
||||
cryptoCurrencyStatus = it
|
||||
coinCryptoCurrencyStatus = it
|
||||
getWalletsAndRecent()
|
||||
uiState = stateFactory.getReadyState()
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(balanceJobHolder)
|
||||
} else {
|
||||
combine(
|
||||
flow = getCoinCurrencyStatusUpdates(isSingleWallet = isSingleWallet),
|
||||
flow2 = getCurrencyStatusUpdates(isSingleWallet = isSingleWallet),
|
||||
) { coinStatus, currencyStatus ->
|
||||
if (coinStatus.isRight() && currencyStatus.isRight()) {
|
||||
coinStatus.onRight { coinCryptoCurrencyStatus = it }
|
||||
currencyStatus.onRight { cryptoCurrencyStatus = it }
|
||||
getWalletsAndRecent()
|
||||
uiState = stateFactory.getReadyState()
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(balanceJobHolder)
|
||||
}.flowWithLifecycle(owner.lifecycle)
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(balanceJobHolder)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCoinCurrencyStatusUpdates(isSingleWallet: Boolean) = getNetworkCoinStatusUseCase(
|
||||
userWalletId = userWalletId,
|
||||
networkId = cryptoCurrency.network.id,
|
||||
derivationPath = cryptoCurrency.network.derivationPath,
|
||||
isSingleWalletWithTokens = isSingleWallet,
|
||||
).conflate().distinctUntilChanged()
|
||||
|
||||
private fun getCurrencyStatusUpdates(isSingleWallet: Boolean) = getCurrencyStatusUpdatesUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
isSingleWalletWithTokens = isSingleWallet,
|
||||
).conflate().distinctUntilChanged()
|
||||
|
||||
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
|
||||
return getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
|
|
@ -255,10 +284,11 @@ internal class SendViewModel @Inject constructor(
|
|||
.onEach {
|
||||
val amountState = uiState.amountState ?: return@onEach
|
||||
val recipientState = uiState.recipientState ?: return@onEach
|
||||
val amount = amountState.amountTextField.value.toBigDecimal()
|
||||
|
||||
stateFactory.onFeeOnLoadingState()
|
||||
uiState = stateFactory.onFeeOnLoadingState()
|
||||
getFeeUseCase.invoke(
|
||||
amount = amountState.amountTextField.value.toBigDecimal(),
|
||||
amount = amount,
|
||||
destination = recipientState.addressTextField.value,
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
|
|
@ -268,7 +298,7 @@ internal class SendViewModel @Inject constructor(
|
|||
.onEach { maybeFee ->
|
||||
maybeFee.fold(
|
||||
ifRight = {
|
||||
stateFactory.onFeeOnLoadedState(it)
|
||||
uiState = stateFactory.onFeeOnLoadedState(it, true)
|
||||
},
|
||||
ifLeft = {
|
||||
// TODO add error handling
|
||||
|
|
@ -298,7 +328,10 @@ internal class SendViewModel @Inject constructor(
|
|||
override fun onQrCodeScanClick() {
|
||||
// TODO Add QR code scanning
|
||||
}
|
||||
// endregion
|
||||
|
||||
override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) =
|
||||
innerRouter.openTokenDetails(userWalletId, currency)
|
||||
// endregion
|
||||
|
||||
// region amount state clicks
|
||||
override fun onCurrencyChangeClick(isFiat: Boolean) {
|
||||
|
|
@ -318,7 +351,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
onAmountValueChange(amount?.toPlainString() ?: DEFAULT_VALUE)
|
||||
}
|
||||
// endregion
|
||||
// endregion
|
||||
|
||||
// region recipient state clicks
|
||||
override fun onRecipientAddressValueChange(value: String) {
|
||||
|
|
@ -362,63 +395,21 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
return false
|
||||
}
|
||||
// endregion
|
||||
// endregion
|
||||
|
||||
//region fee
|
||||
override fun onFeeSelectorClick(feeType: FeeType) {
|
||||
stateFactory.onFeeSelectedState(feeType)
|
||||
updateReceiveAmount()
|
||||
uiState = stateFactory.onFeeSelectedState(feeType)
|
||||
}
|
||||
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {
|
||||
uiState.feeState?.apply {
|
||||
(feeSelectorState.value as? FeeSelectorState.Content)?.let { feeSelector ->
|
||||
feeSelector.customValues.update {
|
||||
it.toMutableList().apply {
|
||||
set(index, it[index].copy(value = value))
|
||||
}
|
||||
}
|
||||
updateReceiveAmount()
|
||||
}
|
||||
}
|
||||
uiState = stateFactory.onCustomFeeValueChange(index, value)
|
||||
}
|
||||
|
||||
override fun onSubtractSelect(value: Boolean) {
|
||||
uiState.feeState?.isSubtract?.update { value }
|
||||
if (value) {
|
||||
updateReceiveAmount()
|
||||
}
|
||||
uiState = stateFactory.onSubtractSelect(value)
|
||||
}
|
||||
|
||||
private fun updateReceiveAmount() {
|
||||
uiState.feeState?.receivedAmount?.update {
|
||||
BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = calculateReceiveAmount(),
|
||||
cryptoCurrency = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateReceiveAmount(): BigDecimal {
|
||||
val feeState = uiState.feeState?.feeSelectorState?.value as? FeeSelectorState.Content ?: return BigDecimal.ZERO
|
||||
val amount = uiState.amountState?.amountTextField?.value ?: return BigDecimal.ZERO
|
||||
|
||||
val fee = when (val selectedFee = feeState.fees) {
|
||||
is TransactionFee.Choosable -> {
|
||||
when (feeState.selectedFee) {
|
||||
FeeType.SLOW -> selectedFee.minimum.amount.value
|
||||
FeeType.MARKET -> selectedFee.normal.amount.value
|
||||
FeeType.FAST -> selectedFee.priority.amount.value
|
||||
FeeType.CUSTOM -> feeState.customValues.value.firstOrNull()?.value?.let { BigDecimal(it) }
|
||||
}
|
||||
}
|
||||
is TransactionFee.Single -> selectedFee.normal.amount.value
|
||||
} ?: BigDecimal.ZERO
|
||||
|
||||
return BigDecimal(amount).minus(fee)
|
||||
}
|
||||
//endregion
|
||||
//endregion
|
||||
|
||||
// region send state clicks
|
||||
override fun onSendClick() {
|
||||
|
|
@ -435,7 +426,7 @@ internal class SendViewModel @Inject constructor(
|
|||
val sendState = uiState.sendState ?: return
|
||||
val amount = uiState.amountState?.amountTextField?.value ?: return
|
||||
val recipient = uiState.recipientState?.addressTextField?.value ?: return
|
||||
val feeState = uiState.feeState?.feeSelectorState?.value as? FeeSelectorState.Content ?: return
|
||||
val feeState = uiState.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return
|
||||
val memo = uiState.recipientState?.memoTextField?.value
|
||||
val fee = getFee(feeState) ?: return
|
||||
|
||||
|
|
@ -487,7 +478,7 @@ internal class SendViewModel @Inject constructor(
|
|||
FeeType.MARKET -> selectedFee.normal
|
||||
FeeType.FAST -> selectedFee.priority
|
||||
FeeType.CUSTOM -> {
|
||||
val feeAmount = feeState.customValues.value.firstOrNull()?.value
|
||||
val feeAmount = feeState.customValues.firstOrNull()?.value
|
||||
?.let { BigDecimal(it) } ?: return null
|
||||
Fee.Common(feeAmount.convertToAmount(cryptoCurrency))
|
||||
}
|
||||
|
|
@ -517,7 +508,7 @@ internal class SendViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
// endregion
|
||||
|
||||
private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? {
|
||||
val blockchain = Blockchain.fromId(networkId)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ internal sealed class ExchangeStatusNotifications(val config: NotificationConfig
|
|||
subtitle = TextReference.Res(R.string.express_exchange_notification_verification_text),
|
||||
iconResId = R.drawable.ic_alert_triangle_20,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = TextReference.Res(R.string.express_go_to_provider),
|
||||
text = TextReference.Res(R.string.common_go_to_provider),
|
||||
onClick = onGoToProviderClick,
|
||||
),
|
||||
),
|
||||
|
|
@ -30,7 +30,7 @@ internal sealed class ExchangeStatusNotifications(val config: NotificationConfig
|
|||
subtitle = TextReference.Res(R.string.express_exchange_notification_failed_text),
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = TextReference.Res(R.string.express_go_to_provider),
|
||||
text = TextReference.Res(R.string.common_go_to_provider),
|
||||
onClick = onGoToProviderClick,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ internal fun ExchangeStatusBlock(
|
|||
.padding(end = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.express_go_to_provider),
|
||||
text = stringResource(id = R.string.common_go_to_provider),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue