Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-26 21:02:13 +03:00
commit 8da4dc8205
23 changed files with 626 additions and 228 deletions

View file

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

View file

@ -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 &amp; 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>

View file

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

View file

@ -61,6 +61,7 @@ dependencies {
/** Feature modules */
implementation(projects.features.send.api)
implementation(projects.features.tokendetails.api)
/** DI */
implementation(deps.hilt.android)

View file

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

View file

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

View file

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

View file

@ -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 */

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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