From 8ceb2914352a2f1ba33d5380b6acc1e400f01797 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Dec 2023 18:44:47 +0300 Subject: [PATCH] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 3 + core/res/src/main/res/values/strings.xml | 4 +- .../DefaultWalletAddressServiceRepository.kt | 10 +- features/send/impl/build.gradle.kts | 1 + .../send/impl/navigation/DefaultSendRouter.kt | 17 ++ .../send/impl/navigation/InnerSendRouter.kt | 5 + .../presentation/state/SendStateFactory.kt | 156 +++++++++++++++--- .../impl/presentation/state/SendUiState.kt | 11 +- .../presentation/state/fee/FeeCalculation.kt | 26 +++ .../state/fee/FeeNotificationFactory.kt | 96 +++++++++++ .../state/fee/FeeSelectorState.kt | 7 +- .../state/fee/SendFeeCustomFieldConverter.kt | 55 +++--- .../state/fee/SendFeeNotification.kt | 78 +++++++++ .../presentation/ui/SendNavigationButtons.kt | 31 +++- .../ui/fee/SendCustomFeeEthereum.kt | 12 +- .../ui/fee/SendSpeedAndFeeContent.kt | 123 ++++++++++---- .../presentation/ui/fee/SendSpeedSelector.kt | 39 +++-- .../presentation/ui/fee/SendSpeedSubtract.kt | 15 +- .../impl/presentation/ui/send/SendContent.kt | 18 +- .../viewmodel/SendClickIntents.kt | 4 + .../presentation/viewmodel/SendViewModel.kt | 137 +++++++-------- .../components/ExchangeStatusNotifications.kt | 4 +- .../exchange/ExchangeStatusBlock.kt | 2 +- 23 files changed, 626 insertions(+), 228 deletions(-) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 6fe946df8b..190b4f48ad 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -98,6 +98,7 @@ Медленно Скорость и комиссия Получить адреса + К провайдеру Импортировать Нравится Заблокирован @@ -449,6 +450,8 @@ Комиссия превышает баланс Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. Увеличение комиссии + Установлена высокая комиссия + Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению Недопустимая сумма Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 6045fd4bd4..4aea85ef4c 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -95,6 +95,7 @@ Market Slow Speed and fee + Go to provider Get addresses Import Learn & Earn @@ -223,7 +224,6 @@ List of all tokens added to your wallet Fetching best rates... Floating rate - Go to provider Provider Best rate Available from %s @@ -473,6 +473,8 @@ Fee exceeds balance The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01. Fee is increased + Custom fee is high + The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. The included commission exceeds the transfer amount, leading to a negative value Invalid amount The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s. diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletAddressServiceRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletAddressServiceRepository.kt index 77ca9b019c..689f2e9c63 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletAddressServiceRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletAddressServiceRepository.kt @@ -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) diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index d45ddc152e..69d2d12a3f 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { /** Feature modules */ implementation(projects.features.send.api) + implementation(projects.features.tokendetails.api) /** DI */ implementation(deps.hilt.android) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index c72ca92ae7..41ddca52e6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -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, + ), + ), + ) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt index b7cd7f934a..1cba26003d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt @@ -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) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 0cdd57f8a2..1f03dddf54 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -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, @@ -34,6 +37,7 @@ internal class SendStateFactory( private val walletAddressesProvider: Provider>, private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + coinCryptoCurrencyStatusProvider: Provider, ) { 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 } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index b7eb3da888..3ff13c540a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -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 = MutableStateFlow(FeeSelectorState.Empty), - val isSubtract: MutableStateFlow = MutableStateFlow(false), - val receivedAmount: MutableStateFlow = MutableStateFlow(""), + val feeSelectorState: FeeSelectorState = FeeSelectorState.Loading, + val isSubtract: Boolean = false, + val receivedAmount: String = "", + val notifications: ImmutableList = persistentListOf(), + val isPrimaryButtonEnabled: Boolean = false, ) : SendStates() /** Send state */ diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt new file mode 100644 index 0000000000..263f4df2c0 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt @@ -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) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt new file mode 100644 index 0000000000..f9706b0d17 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt @@ -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, + private val userWalletProvider: Provider, + private val clickIntents: SendClickIntents, +) { + + operator fun invoke(feeState: SendStates.FeeState): ImmutableList { + 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.addTooLowNotification( + transactionFee: TransactionFee, + selectedFee: FeeType, + customFee: List, + ) { + 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.addTooHighNotification( + transactionFee: TransactionFee, + selectedFee: FeeType, + customFee: List, + ) { + 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.addFeeCoverageNotification() { + // TODO add fee coverage condition [REDACTED_JIRA] + add(SendFeeNotification.Warning.NetworkCoverage) + } + + private fun MutableList.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) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt index f920348ac8..c5a392c4a9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt @@ -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> = MutableStateFlow(emptyList()), + val customValues: ImmutableList = persistentListOf(), ) : FeeSelectorState() } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt index 858fc11a49..ff9da4a71f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt @@ -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, -) : Converter>> { +) : Converter> { - override fun convert(value: Fee): MutableStateFlow> { - val ethereumFee = value as? Fee.Ethereum ?: return MutableStateFlow(emptyList()) + override fun convert(value: Fee): ImmutableList { + 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, ), ), ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt new file mode 100644 index 0000000000..8763cf5cc2 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt @@ -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, + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index e8118443a5..2f17471513 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -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, 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 + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt index 98b24927f0..69bc85a649 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt @@ -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>, + customValues: ImmutableList, 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), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt index 595d04491a..4c8c06116d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -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, 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, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt index de69e011d6..f221817a1d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -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, + 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 } } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt index fac746da5b..5fa7355bce 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt @@ -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, - isSubtract: StateFlow, + 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, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index 9726adff7c..e488e1a3e0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -166,18 +166,18 @@ private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess: @Composable private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: State, 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() } }, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index d29ab95bee..f79bf242b5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -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) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 59a21072b0..8797df79a7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -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
() @@ -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 { 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) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt index 1da8fd0672..ac825983b8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt @@ -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, ), ), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt index 9d44f11a9e..f5196025aa 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt @@ -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, )