From 0b037f2b2da7d7da9d7d9233eb583ae80babb1e5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Jan 2024 16:20:14 +0300 Subject: [PATCH 1/4] Updated on 2026-08-14 --- .../common/redux/legacy/LegacyMiddleware.kt | 6 ++ .../tap/di/domain/TransactionDomainModule.kt | 4 +- core/res/src/main/res/values-ru/strings.xml | 5 +- .../src/main/res/values-zh-rTW/strings.xml | 1 - core/res/src/main/res/values/strings.xml | 6 +- .../DefaultTransactionRepository.kt | 20 +++- .../com/tangem/domain/redux/LegacyAction.kt | 5 + domain/transaction/build.gradle.kts | 4 + .../transaction/TransactionRepository.kt | 9 ++ .../transaction/error/SendTransactionError.kt | 9 +- .../usecase/SendTransactionUseCase.kt | 47 +++++----- features/send/impl/build.gradle.kts | 1 + .../impl/presentation/state/SendAlertState.kt | 47 ++++++++++ .../send/impl/presentation/state/SendEvent.kt | 12 +++ .../state/SendEventStateFactory.kt | 50 ++++++++++ .../presentation/state/SendStateFactory.kt | 3 +- .../state/SendTransactionAlertConverter.kt | 44 +++++++++ .../impl/presentation/state/SendUiState.kt | 2 + .../impl/presentation/ui/SendEventEffect.kt | 73 +++++++++++++++ .../send/impl/presentation/ui/SendScreen.kt | 8 ++ .../viewmodel/SendClickIntents.kt | 3 + .../presentation/viewmodel/SendViewModel.kt | 93 +++++++++++-------- .../swap/domain/di/SwapDomainModule.kt | 5 +- .../tangem/feature/swap/ui/StateBuilder.kt | 2 +- .../feature/swap/ui/SwapScreenContent.kt | 4 +- gradle/dependencies.toml | 2 +- 26 files changed, 385 insertions(+), 80 deletions(-) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEvent.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index 81d40b5307..9ad974cdcd 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.legacy import com.tangem.domain.redux.LegacyAction import com.tangem.tap.common.feedback.RateCanBeBetterEmail +import com.tangem.tap.common.feedback.SendTransactionFailedEmail import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.store @@ -20,6 +21,11 @@ internal object LegacyMiddleware { GlobalAction.Onboarding.Start(action.scanResponse, canSkipBackup = action.canSkipBackup), ) } + is LegacyAction.SendEmailTransactionFailed -> { + store.state.globalState.feedbackManager?.sendEmail( + SendTransactionFailedEmail(action.errorMessage), + ) + } } next(action) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 5429ab3030..fdd81bd7d9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -31,13 +31,13 @@ internal object TransactionDomainModule { @ViewModelScoped fun provideSendTransactionUseCase( isDemoCardUseCase: IsDemoCardUseCase, - walletManagersFacade: WalletManagersFacade, cardSdkConfigRepository: CardSdkConfigRepository, + transactionRepository: TransactionRepository, ): SendTransactionUseCase { return SendTransactionUseCase( isDemoCardUseCase = isDemoCardUseCase, cardSdkConfigRepository = cardSdkConfigRepository, - walletManagersFacade = walletManagersFacade, + transactionRepository = transactionRepository, ) } diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 12777954f8..dc64c4dac6 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -136,6 +136,7 @@ Перевод Я понял Недоступно + Произошла ошибка. Пожалуйста, попробуйте снова. Да Адрес контракта скопирован! Доступные сети @@ -448,8 +449,9 @@ Сумма Вычесть из суммы отправки Сумма к получению %s + Поддержка Транзакция не выполнена - Причина: %1$s\Код:%2$s + Причина: %1$s\nКод: %2$s %1$s в %2$s Адрес Код назначения @@ -528,7 +530,6 @@ Подтвердить Ошибка: %s Вы отправляете - Произошла ошибка. Пожалуйста, попробуйте еще раз. Дать разрешение Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. Недостаточно средств diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index c7ef19cf31..a1d2ee341a 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -327,7 +327,6 @@ 批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。 批准 錯誤: %s - 有錯誤。請再試一遍 賦予權限 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量 餘額不足 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 585432c7dd..b0bd11e551 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -135,6 +135,7 @@ Transfer I understand Unreachable + There was an error. Please try again. Yes Contract address copied! Available networks @@ -447,8 +448,9 @@ Amount Subtract from send amount The recipient will receive %s + Support The transaction is not completed - Reason: %1$s\nCode:%2$s + Reason: %1$s\nCode: %2$s Confirm %1$s at %2$s Address @@ -519,6 +521,7 @@ Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while Invalid address Transaction sent + %s (%s) Buy now I have a promo code… Tangem Wallet @@ -545,7 +548,6 @@ Approve Error: %s You swap - There was an error. Please try again. Give Permission Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index e5cf1cec6c..7c515253c2 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -7,10 +7,7 @@ import com.tangem.blockchain.blockchains.stellar.StellarMemo import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.ton.TonTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.TransactionExtras +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository @@ -44,6 +41,21 @@ internal class DefaultTransactionRepository( ) } + override suspend fun sendTransaction( + txData: TransactionData, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ) = withContext(coroutineDispatcherProvider.io) { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + (walletManager as TransactionSender).send(txData, signer) + } + private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { val blockchain = Blockchain.fromId(networkId) if (memo == null) return null diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt index f715061ac7..826ba5a966 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt @@ -13,4 +13,9 @@ sealed interface LegacyAction : Action { * BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup */ data class StartOnboardingProcess(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : LegacyAction + + /** + * Sending an email to support when sending transaction failed + */ + data class SendEmailTransactionFailed(val errorMessage: String) : LegacyAction } \ No newline at end of file diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index 571b6c0f50..f0345df888 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -13,9 +13,13 @@ dependencies { implementation(deps.arrow.core) implementation(projects.core.utils) + implementation(projects.core.ui) /** Tangem SDKs */ implementation(deps.tangem.card.core) + implementation(deps.tangem.card.android) { + exclude(module = "joda-time") + } implementation(deps.tangem.blockchain) implementation(projects.domain.models) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 6c38bc0e7d..70c8b7d0ae 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -1,8 +1,10 @@ package com.tangem.domain.transaction import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.CommonSigner import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.extensions.SimpleResult import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -17,4 +19,11 @@ interface TransactionRepository { userWalletId: UserWalletId, network: Network, ): TransactionData? + + suspend fun sendTransaction( + txData: TransactionData, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ): SimpleResult } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt index 8ed9705902..2d5536cefb 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt @@ -1,5 +1,7 @@ package com.tangem.domain.transaction.error +import com.tangem.core.ui.extensions.TextReference + sealed class SendTransactionError { object DemoCardError : SendTransactionError() @@ -8,9 +10,12 @@ sealed class SendTransactionError { data class NetworkError(val message: String?) : SendTransactionError() - data class BlockchainSdkError(val code: Int, val cause: Throwable?) : SendTransactionError() + data class BlockchainSdkError(val code: Int, val message: String?) : SendTransactionError() + object UserCancelledError : SendTransactionError() - data class TangemSdkError(val code: Int, val cause: Throwable?) : SendTransactionError() + + data class TangemSdkError(val code: Int, val messageReference: TextReference) : SendTransactionError() + data class UnknownError(val ex: Exception? = null) : SendTransactionError() companion object { diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 1e710033ae..c8ed1fe905 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -8,19 +8,23 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.network.ResultChecker import com.tangem.common.core.TangemSdkError +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.R +import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet +import com.tangem.sdk.extensions.localizedDescriptionRes class SendTransactionUseCase( private val isDemoCardUseCase: IsDemoCardUseCase, private val cardSdkConfigRepository: CardSdkConfigRepository, - private val walletManagersFacade: WalletManagersFacade, + private val transactionRepository: TransactionRepository, ) { suspend operator fun invoke( txData: TransactionData, @@ -37,7 +41,7 @@ class SendTransactionUseCase( if (isDemoCardUseCase(cardId = userWallet.cardId)) { SendTransactionError.DemoCardError.left() } else { - walletManagersFacade.sendTransaction( + transactionRepository.sendTransaction( txData = txData, signer = signer, userWalletId = userWallet.walletId, @@ -64,29 +68,28 @@ class SendTransactionUseCase( private fun handleError(result: SimpleResult.Failure): SendTransactionError { if (ResultChecker.isNetworkError(result)) return SendTransactionError.NetworkError(result.error.message) val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError() - when (error) { + return when (error) { is BlockchainSdkError.WrappedTangemError -> { - val errorByCode = mapErrorByCode(error) - if (errorByCode != null) { - return errorByCode + if (error.code == USER_CANCELLED_ERROR_CODE) { + SendTransactionError.UserCancelledError + } else { + val tangemError = error.tangemError + if (tangemError is TangemSdkError) { + val resource = tangemError.localizedDescriptionRes() + val resId = resource.resId ?: R.string.common_unknown_error + val resArgs = resource.args.map { it.value } + val textReference = resourceReference(resId, wrappedList(resArgs)) + SendTransactionError.TangemSdkError(tangemError.code, textReference) + } else { + SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage) + } } - val tangemSdkError = error.tangemError as? TangemSdkError ?: return SendTransactionError.UnknownError() - if (tangemSdkError is TangemSdkError.UserCancelled) return SendTransactionError.UserCancelledError - return SendTransactionError.TangemSdkError(tangemSdkError.code, tangemSdkError.cause) } else -> { - return SendTransactionError.TangemSdkError(error.code, error.cause) - } - } - } - - private fun mapErrorByCode(error: BlockchainSdkError.WrappedTangemError): SendTransactionError? { - return when (error.code) { - USER_CANCELLED_ERROR_CODE -> { - return SendTransactionError.UserCancelledError - } - else -> { - null + SendTransactionError.BlockchainSdkError( + code = error.code, + message = error.customMessage, + ) } } } diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 53a6d8a573..46295abadb 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(deps.lifecycle.compose) implementation(deps.jodatime) implementation(deps.timber) + implementation(deps.reKotlin) /** Compose */ implementation(deps.compose.accompanist.systemUiController) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt new file mode 100644 index 0000000000..ff2aac684f --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt @@ -0,0 +1,47 @@ +package com.tangem.features.send.impl.presentation.state + +import androidx.compose.runtime.Immutable +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 +internal sealed class SendAlertState { + + abstract val title: TextReference? + abstract val message: TextReference + open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + open val onConfirmClick: (() -> Unit)? = null + + data class GenericError( + override val title: TextReference? = resourceReference(id = R.string.send_alert_transaction_failed_title), + override val onConfirmClick: (() -> Unit), + ) : SendAlertState() { + override val message: TextReference = resourceReference(R.string.common_unknown_error) + override val confirmButtonText: TextReference = + resourceReference(id = R.string.send_alert_button_request_support) + } + + data class TransactionError( + val code: String, + val cause: String?, + val causeTextReference: TextReference? = null, + override val onConfirmClick: (() -> Unit), + ) : SendAlertState() { + override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title) + override val message: TextReference = resourceReference( + id = R.string.send_alert_transaction_failed_text, + formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code), + ) + override val confirmButtonText: TextReference = + resourceReference(id = R.string.send_alert_button_request_support) + } + + data class DemoMode( + override val onConfirmClick: () -> Unit, + ) : SendAlertState() { + override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title) + override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEvent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEvent.kt new file mode 100644 index 0000000000..2fdb687d96 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEvent.kt @@ -0,0 +1,12 @@ +package com.tangem.features.send.impl.presentation.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class SendEvent { + + data class ShowSnackBar(val text: TextReference) : SendEvent() + + data class ShowAlert(val alert: SendAlertState) : SendEvent() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt new file mode 100644 index 0000000000..8bd869cb7c --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt @@ -0,0 +1,50 @@ +package com.tangem.features.send.impl.presentation.state + +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Provider + +/** + * Factory to produce event state for [SendUiState] + * + * @param currentStateProvider [Provider] of [SendUiState] + * @param clickIntents [SendClickIntents] + */ +internal class SendEventStateFactory( + val currentStateProvider: Provider, + val clickIntents: SendClickIntents, +) { + private val sendTransactionErrorConverter by lazy { SendTransactionAlertConverter(clickIntents) } + + fun onConsumeEventState(): SendUiState { + return currentStateProvider().copy(event = consumedEvent()) + } + + fun getSendTransactionErrorState(error: SendTransactionError?, onConsume: () -> Unit): SendUiState { + val state = currentStateProvider() + val event = error?.let { + sendTransactionErrorConverter.convert(error)?.let { + triggeredEvent(SendEvent.ShowAlert(it), onConsume) + } + } + return state.copy( + event = event ?: consumedEvent(), + ) + } + + fun getGenericErrorState(error: Throwable? = null, onConsume: () -> Unit): SendUiState { + val state = currentStateProvider() + return state.copy( + event = triggeredEvent( + data = SendEvent.ShowAlert( + SendAlertState.GenericError( + onConfirmClick = { clickIntents.onFailedTxEmailClick(error?.localizedMessage.orEmpty()) }, + ), + ), + onConsume = onConsume, + ), + ) + } +} \ 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 70fac6ad0d..4fea850423 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 arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency @@ -44,7 +45,6 @@ internal class SendStateFactory( ) { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) - private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) } private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) } private val customFeeFieldConverter by lazy { @@ -92,6 +92,7 @@ internal class SendStateFactory( fun getInitialState(): SendUiState = SendUiState( clickIntents = clickIntents, currentState = MutableStateFlow(SendUiStateType.Amount), + event = consumedEvent(), ) fun getReadyState(): SendUiState { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt new file mode 100644 index 0000000000..14f9cc265d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt @@ -0,0 +1,44 @@ +package com.tangem.features.send.impl.presentation.state + +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter + +internal class SendTransactionAlertConverter( + private val clickIntents: SendClickIntents, +) : Converter { + override fun convert(value: SendTransactionError): SendAlertState? { + return when (value) { + SendTransactionError.DemoCardError -> SendAlertState.DemoMode( + onConfirmClick = { clickIntents.popBackStack() }, + ) + is SendTransactionError.TangemSdkError -> SendAlertState.TransactionError( + code = value.code.toString(), + cause = null, + causeTextReference = value.messageReference, + onConfirmClick = { clickIntents.onFailedTxEmailClick(value.code.toString()) }, + ) + is SendTransactionError.BlockchainSdkError -> SendAlertState.TransactionError( + code = value.code.toString(), + cause = value.message, + onConfirmClick = { clickIntents.onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") }, + ) + is SendTransactionError.DataError -> SendAlertState.TransactionError( + code = "", + cause = value.message, + onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) }, + ) + is SendTransactionError.NetworkError -> SendAlertState.TransactionError( + code = "", + cause = value.message, + onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) }, + ) + is SendTransactionError.UnknownError -> SendAlertState.TransactionError( + code = "", + cause = value.ex?.localizedMessage, + onConfirmClick = { clickIntents.onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) }, + ) + else -> null + } + } +} \ 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 fb2918c11d..3db22025d6 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 @@ -5,6 +5,7 @@ import androidx.compose.runtime.Stable import androidx.paging.PagingData import com.tangem.blockchain.common.transaction.Fee import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.event.StateEvent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent @@ -31,6 +32,7 @@ internal data class SendUiState( val sendState: SendStates.SendState = SendStates.SendState(), val recipientList: MutableStateFlow> = MutableStateFlow(PagingData.empty()), val currentState: MutableStateFlow, + val event: StateEvent, ) @Stable diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt new file mode 100644 index 0000000000..84e638eb8a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt @@ -0,0 +1,73 @@ +package com.tangem.features.send.impl.presentation.ui + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendAlertState +import com.tangem.features.send.impl.presentation.state.SendEvent + +@Composable +internal fun SendEventEffect(event: StateEvent, snackbarHostState: SnackbarHostState) { + val resources = LocalContext.current.resources + var alertConfig by remember { mutableStateOf(value = null) } + + alertConfig?.let { + SendAlert(state = it, onDismiss = { alertConfig = null }) + } + + EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is SendEvent.ShowSnackBar -> { + snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) + } + is SendEvent.ShowAlert -> { + alertConfig = value.alert + } + } + }, + ) +} + +@Composable +internal fun SendAlert(state: SendAlertState, onDismiss: () -> Unit) { + val confirmButton: DialogButton + val dismissButton: DialogButton? + + val onActionClick = state.onConfirmClick + if (onActionClick != null) { + confirmButton = DialogButton( + title = state.confirmButtonText.resolveReference(), + onClick = { + onActionClick() + onDismiss() + }, + ) + dismissButton = DialogButton( + title = stringResource(id = R.string.common_cancel), + onClick = onDismiss, + ) + } else { + confirmButton = DialogButton( + title = state.confirmButtonText.resolveReference(), + onClick = onDismiss, + ) + dismissButton = null + } + + BasicDialog( + message = state.message.resolveReference(), + confirmButton = confirmButton, + onDismissDialog = onDismiss, + title = state.title?.resolveReference(), + dismissButton = dismissButton, + ) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index eae5388921..8fe5c5f53d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -7,8 +7,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.State +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -28,6 +30,7 @@ import com.tangem.features.send.impl.presentation.ui.send.SendContent internal fun SendScreen(uiState: SendUiState) { val currentState = uiState.currentState.collectAsStateWithLifecycle() val isSuccess = uiState.sendState.isSuccess + val snackbarHostState = remember { SnackbarHostState() } BackHandler { uiState.clickIntents.onBackClick() } Column( modifier = Modifier @@ -65,6 +68,11 @@ internal fun SendScreen(uiState: SendUiState) { ) SendNavigationButtons(uiState) } + + SendEventEffect( + event = uiState.event, + snackbarHostState = snackbarHostState, + ) } @Composable 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 f79bf242b5..43a116ee65 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 @@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.send.impl.presentation.state.fee.FeeType +@Suppress("TooManyFunctions") interface SendClickIntents { fun popBackStack() @@ -16,6 +17,8 @@ interface SendClickIntents { fun onQrCodeScanClick() + fun onFailedTxEmailClick(errorMessage: String) + fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) // region Amount 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 133e865e26..d4861a2e74 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 @@ -8,8 +8,11 @@ import androidx.paging.PagingData import arrow.core.getOrElse import com.tangem.blockchain.blockchains.xrp.XrpAddressService import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionData import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.redux.LegacyAction +import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase @@ -31,10 +34,6 @@ import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.* -import com.tangem.features.send.impl.presentation.state.SendNotificationFactory -import com.tangem.features.send.impl.presentation.state.SendStateFactory -import com.tangem.features.send.impl.presentation.state.SendUiState -import com.tangem.features.send.impl.presentation.state.StateRouter 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.getFee @@ -70,6 +69,7 @@ internal class SendViewModel @Inject constructor( private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, private val parseSharedAddressUseCase: ParseSharedAddressUseCase, private val walletManagersFacade: WalletManagersFacade, + private val reduxStateHolder: ReduxStateHolder, getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, validateWalletMemoUseCase: ValidateWalletMemoUseCase, savedStateHandle: SavedStateHandle, @@ -98,6 +98,11 @@ internal class SendViewModel @Inject constructor( getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase, ) + private val eventStateFactory = SendEventStateFactory( + clickIntents = this, + currentStateProvider = Provider { uiState }, + ) + private val sendNotificationFactory = SendNotificationFactory( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, @@ -106,6 +111,7 @@ internal class SendViewModel @Inject constructor( walletManagersFacade = walletManagersFacade, ) + // todo convert to StateFlow var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState()) private set @@ -139,7 +145,9 @@ internal class SendViewModel @Inject constructor( getCurrenciesStatusUpdates(owner, wallet) }, ifLeft = { - // todo add error handling [[REDACTED_JIRA]] + uiState = eventStateFactory.getGenericErrorState( + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) return@launch }, ) @@ -331,6 +339,10 @@ internal class SendViewModel @Inject constructor( override fun onQrCodeScanClick() = innerRouter.openQrCodeScanner(cryptoCurrency.network.name) + override fun onFailedTxEmailClick(errorMessage: String) { + reduxStateHolder.dispatch(LegacyAction.SendEmailTransactionFailed(errorMessage)) + } + override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) = innerRouter.openTokenDetails(userWalletId, currency) // endregion @@ -433,8 +445,8 @@ internal class SendViewModel @Inject constructor( val sendState = uiState.sendState if (sendState.isSuccess) popBackStack() - uiState = stateFactory.getSendingStateUpdate(true) - viewModelScope.launch(dispatchers.io) { verifyAndSendTransaction() } + uiState = stateFactory.getSendingStateUpdate(isSending = true) + verifyAndSendTransaction() } override fun showAmount() = stateRouter.showAmount(isFromSend = true) @@ -445,7 +457,7 @@ internal class SendViewModel @Inject constructor( override fun onExploreClick(txUrl: String) = innerRouter.openUrl(txUrl) - private suspend fun verifyAndSendTransaction() { + private fun verifyAndSendTransaction() { val recipient = uiState.recipientState?.addressTextField?.value ?: return val feeState = uiState.feeState ?: return val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return @@ -454,41 +466,46 @@ internal class SendViewModel @Inject constructor( val amountToSend = feeState.receivedAmountValue.convertToAmount(cryptoCurrency) - // todo add error handling [[REDACTED_JIRA]] - // val transactionErrors = walletManagersFacade.validateTransaction( - // amount = amountToSend, - // fee = fee.amount, - // userWalletId = userWalletId, - // network = cryptoCurrency.network, - // ) + viewModelScope.launch(dispatchers.main) { + createTransactionUseCase( + amount = amountToSend, + fee = fee, + memo = memo, + destination = recipient, + userWalletId = userWalletId, + network = cryptoCurrency.network, + ).fold( + ifLeft = { + Timber.e(it) + uiState = eventStateFactory.getGenericErrorState( + error = it, + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + }, + ifRight = { txData -> + sendTransaction(txData) + }, + ) + } + } - createTransactionUseCase( - amount = amountToSend, - fee = fee, - memo = memo, - destination = recipient, - userWalletId = userWalletId, + private suspend fun sendTransaction(txData: TransactionData) { + sendTransactionUseCase( + txData = txData, + userWallet = userWallet, network = cryptoCurrency.network, ).fold( - ifLeft = { - Timber.e(it) - // todo add error handling [[REDACTED_JIRA]] - }, - ifRight = { txData -> - sendTransactionUseCase( - txData = txData, - userWallet = userWallet, - network = cryptoCurrency.network, - ).fold( - ifLeft = { - uiState = stateFactory.getSendingStateUpdate(false) - // todo add error handling [[REDACTED_JIRA]] - }, - ifRight = { - uiState = stateFactory.getTransactionSendState(txData) - }, + ifLeft = { error -> + uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState = eventStateFactory.getSendTransactionErrorState( + error = error, + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, ) }, + ifRight = { + uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState = stateFactory.getTransactionSendState(txData) + }, ) } // endregion diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index d299f7824e..bfa910c669 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -8,6 +8,7 @@ import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder @@ -118,13 +119,13 @@ class SwapDomainModule { @Singleton fun provideSendTransactionUseCase( @SwapScope isDemoCardUseCase: IsDemoCardUseCase, - walletManagersFacade: WalletManagersFacade, cardSdkConfigRepository: CardSdkConfigRepository, + transactionRepository: TransactionRepository, ): SendTransactionUseCase { return SendTransactionUseCase( isDemoCardUseCase = isDemoCardUseCase, cardSdkConfigRepository = cardSdkConfigRepository, - walletManagersFacade = walletManagersFacade, + transactionRepository = transactionRepository, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index d0d611881d..90e2f34c73 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -553,7 +553,7 @@ internal class StateBuilder( resourceReference(R.string.warning_express_refresh_required_title) }, subtitle = if (dataError is DataError.UnknownError) { - resourceReference(R.string.swapping_generic_error) + resourceReference(R.string.common_unknown_error) } else { resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString())) }, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index bb85650ace..3b42cf78a6 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -109,7 +109,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi val message = if (state.alert.type == GenericWarningType.NETWORK) { stringResource(id = R.string.disclaimer_error_loading) } else { - state.alert.message?.resolveReference() ?: stringResource(id = R.string.swapping_generic_error) + state.alert.message?.resolveReference() ?: stringResource(id = R.string.common_unknown_error) } SimpleOkDialog( message = message, @@ -338,7 +338,7 @@ private fun SwapWarnings(warnings: List) { } else { it.resolveReference() } - } ?: stringResource(id = R.string.swapping_generic_error) + } ?: stringResource(id = R.string.common_unknown_error) RefreshableWaringCard( title = stringResource(id = R.string.common_warning), description = message, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 2fcde13f2c..3d15ad3489 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -87,7 +87,7 @@ spr-client = "3.6.2" # region Tangem tangemBlockchainSdk = "develop-458" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-318" +tangemCardSdk = "develop-319" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem From 5d69093f1d3773e18ad83ba4299450c2835b57bf Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jan 2024 16:31:19 +0300 Subject: [PATCH 2/4] Updated on 2026-08-14 --- .../usecase/CreateTransactionUseCase.kt | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt index 55c5e0cd88..55f503cf15 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt @@ -2,13 +2,17 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext class CreateTransactionUseCase( private val transactionRepository: TransactionRepository, + private val dispatchers: CoroutineDispatcherProvider, ) { /** @@ -22,16 +26,18 @@ class CreateTransactionUseCase( destination: String, userWalletId: UserWalletId, network: Network, - ) = Either.catch { - requireNotNull( - transactionRepository.createTransaction( - amount = amount, - fee = fee, - memo = memo, - destination = destination, - userWalletId = userWalletId, - network = network, - ), - ) { "Failed to create transaction" } + ): Either = withContext(dispatchers.io) { + Either.catch { + requireNotNull( + transactionRepository.createTransaction( + amount = amount, + fee = fee, + memo = memo, + destination = destination, + userWalletId = userWalletId, + network = network, + ), + ) { "Failed to create transaction" } + } } } \ No newline at end of file From 7ef0543a78ce09d1939c4f934cd205c06e3c0ef4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Jan 2024 15:59:14 +0300 Subject: [PATCH 3/4] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 4 +- core/res/src/main/res/values/strings.xml | 4 +- .../usecase/CreateTransactionUseCase.kt | 28 ++++------ .../presentation/state/SendNotification.kt | 14 ++++- .../state/SendNotificationFactory.kt | 52 +++++++++++++++---- .../impl/presentation/state/SendUiState.kt | 1 + .../impl/presentation/ui/send/SendContent.kt | 5 +- .../viewmodel/SendClickIntents.kt | 4 ++ .../presentation/viewmodel/SendViewModel.kt | 11 ++++ 9 files changed, 91 insertions(+), 32 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index dc64c4dac6..efdc1b3d26 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -479,8 +479,10 @@ Недостаточно средств Размер комиссии превышает баланс сети. Для продолжения необходимо пополнить баланс сети. Комиссия превышает баланс - Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. Увеличение комиссии + Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. + Оставить %s XTZ + Отправить все Установлена высокая комиссия Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b0bd11e551..125fb77911 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -484,8 +484,10 @@ Total exceeds balance The commission fee exceeds the network balance. To continue, it is necessary to replenish the network balance. Fee exceeds balance - The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01. Fee is increased + The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01. + Reduce by %s XTZ + No, send all 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 diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt index 55f503cf15..55c5e0cd88 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt @@ -2,17 +2,13 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext class CreateTransactionUseCase( private val transactionRepository: TransactionRepository, - private val dispatchers: CoroutineDispatcherProvider, ) { /** @@ -26,18 +22,16 @@ class CreateTransactionUseCase( destination: String, userWalletId: UserWalletId, network: Network, - ): Either = withContext(dispatchers.io) { - Either.catch { - requireNotNull( - transactionRepository.createTransaction( - amount = amount, - fee = fee, - memo = memo, - destination = destination, - userWalletId = userWalletId, - network = network, - ), - ) { "Failed to create transaction" } - } + ) = Either.catch { + requireNotNull( + transactionRepository.createTransaction( + amount = amount, + fee = fee, + memo = memo, + destination = destination, + userWalletId = userWalletId, + network = network, + ), + ) { "Failed to create transaction" } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index 6d2975319a..cdbd005127 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -57,16 +57,28 @@ internal sealed class SendNotification(val config: NotificationConfig) { sealed class Warning( title: TextReference, subtitle: TextReference, + buttonsState: NotificationConfig.ButtonsState? = null, ) : SendNotification( config = NotificationConfig( title = title, subtitle = subtitle, iconResId = R.drawable.img_attention_20, + buttonsState = buttonsState, ), ) { - data class HighFeeError(val amount: String) : Warning( + data class HighFeeError( + val amount: String, + val onConfirmClick: () -> Unit, + val onDismissClick: () -> Unit, + ) : Warning( title = resourceReference(R.string.send_notification_high_fee_title), subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)), + buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig( + primaryText = resourceReference(R.string.send_notification_fee_too_high_accept, wrappedList(amount)), + onPrimaryClick = onConfirmClick, + secondaryText = resourceReference(R.string.send_notification_fee_too_high_ignore), + onSecondaryClick = onDismissClick, + ), ) data class ExistentialDeposit(val deposit: String) : Warning( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt index ce29bd2c60..18e0b4f34c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt @@ -6,6 +6,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -21,6 +22,7 @@ internal class SendNotificationFactory( private val currentStateProvider: Provider, private val userWalletProvider: Provider, private val walletManagersFacade: WalletManagersFacade, + private val clickIntents: SendClickIntents, ) { fun create(): Flow> = currentStateProvider().currentState @@ -30,19 +32,33 @@ internal class SendNotificationFactory( val feeState = state.feeState ?: return@map persistentListOf() val recipientState = state.recipientState ?: return@map persistentListOf() val feeAmount = feeState.fee?.amount?.value ?: BigDecimal.ZERO + val amountValue = state.amountState?.amountTextField?.value?.toBigDecimalOrNull() ?: BigDecimal.ZERO + val sendAmount = if (feeState.isSubtract) feeState.receivedAmountValue else amountValue buildList { // errors - addExceedBalanceNotification(feeAmount, feeState.receivedAmountValue) - addInvalidAmountNotification(feeState.isSubtract, feeState.receivedAmountValue) - addMinimumAmountErrorNotification(feeAmount, feeState.receivedAmountValue) + addExceedBalanceNotification(feeAmount, sendAmount) + addInvalidAmountNotification(feeState.isSubtract, sendAmount) + addMinimumAmountErrorNotification(feeAmount, sendAmount) addReserveAmountErrorNotification(recipientState.addressTextField.value) - addTransactionLimitErrorNotification(feeAmount, feeState.receivedAmountValue) + addTransactionLimitErrorNotification(feeAmount, sendAmount) // warnings - addExistentialWarningNotification(feeAmount, feeState.receivedAmountValue) - addHighFeeWarningNotification() + addExistentialWarningNotification(feeAmount, sendAmount) + addHighFeeWarningNotification(amountValue, state.sendState.ignoreAmountReduce) }.toImmutableList() } + fun dismissHighFeeWarningState(): SendUiState { + val state = currentStateProvider() + val sendState = state.sendState + val updatedNotifications = sendState.notifications.filterNot { it is SendNotification.Warning.HighFeeError } + return state.copy( + sendState = sendState.copy( + ignoreAmountReduce = true, + notifications = updatedNotifications.toImmutableList(), + ), + ) + } + private fun MutableList.addExceedBalanceNotification( feeAmount: BigDecimal, receivedAmount: BigDecimal, @@ -173,16 +189,30 @@ internal class SendNotificationFactory( } } - private fun MutableList.addHighFeeWarningNotification() { - // TODO Move Blockchain check elsewhere - if (cryptoCurrencyStatusProvider().currency.network.id.value == Blockchain.Tezos.id) { - add(SendNotification.Warning.HighFeeError(TEZOS_FEE_THRESHOLD)) + private fun MutableList.addHighFeeWarningNotification( + sendAmount: BigDecimal, + ignoreAmountReduce: Boolean, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + val isTezos = cryptoCurrencyStatus.currency.network.id.value == Blockchain.Tezos.id + if (!ignoreAmountReduce && sendAmount == balance && isTezos) { + add( + SendNotification.Warning.HighFeeError( + amount = TEZOS_FEE_THRESHOLD.toPlainString(), + onConfirmClick = { + val reduceTo = sendAmount.minus(TEZOS_FEE_THRESHOLD).toPlainString() + clickIntents.onAmountReduceClick(reduceTo) + }, + onDismissClick = clickIntents::onAmountReduceIgnoreClick, + ), + ) } } companion object { private const val CARDANO_MINIMUM = "1" private const val DOGECOIN_MINIMUM = "0.01" - private const val TEZOS_FEE_THRESHOLD = "0.01" + private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01") } } \ 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 3db22025d6..46526153e8 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 @@ -88,6 +88,7 @@ internal sealed class SendStates { val isSuccess: Boolean = false, val transactionDate: Long = 0L, val txUrl: String = "", + val ignoreAmountReduce: Boolean = false, val notifications: ImmutableList = persistentListOf(), ) : SendStates() } 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 b9f84d7264..ccff5dd66b 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 @@ -197,7 +197,10 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.background.action + else -> TangemTheme.colors.button.disabled + }, iconTint = when (it) { is SendNotification.Error -> TangemTheme.colors.icon.warning is SendNotification.Warning -> null 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 43a116ee65..90e3049f47 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 @@ -53,5 +53,9 @@ interface SendClickIntents { fun showFee() fun onExploreClick(txUrl: String) + + fun onAmountReduceClick(reducedAmount: String) + + fun onAmountReduceIgnoreClick() // endregion } \ No newline at end of file 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 d4861a2e74..151eea4275 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 @@ -109,6 +109,7 @@ internal class SendViewModel @Inject constructor( currentStateProvider = Provider { uiState }, userWalletProvider = Provider { userWallet }, walletManagersFacade = walletManagersFacade, + clickIntents = this, ) // todo convert to StateFlow @@ -457,6 +458,16 @@ internal class SendViewModel @Inject constructor( override fun onExploreClick(txUrl: String) = innerRouter.openUrl(txUrl) + override fun onAmountReduceClick(reducedAmount: String) { + uiState = stateFactory.getOnAmountValueChange(reducedAmount) + uiState = sendNotificationFactory.dismissHighFeeWarningState() + getFee() + } + + override fun onAmountReduceIgnoreClick() { + uiState = sendNotificationFactory.dismissHighFeeWarningState() + } + private fun verifyAndSendTransaction() { val recipient = uiState.recipientState?.addressTextField?.value ?: return val feeState = uiState.feeState ?: return From a614d95d65d03b10dcc855b0e1dc541374fd7bb6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Jan 2024 19:01:31 +0300 Subject: [PATCH 4/4] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 8 +- .../transaction/usecase/GetFeeUseCase.kt | 32 ++- .../presentation/state/SendStateFactory.kt | 161 +------------ .../impl/presentation/state/SendUiState.kt | 2 + .../state/amount/SendAmountStateConverter.kt | 2 + .../presentation/state/fee/FeeCalculation.kt | 10 +- .../state/fee/FeeNotificationFactory.kt | 60 +++-- .../state/fee/FeeSelectorState.kt | 2 + .../presentation/state/fee/FeeStateFactory.kt | 217 ++++++++++++++++++ .../state/fee/SendFeeNotification.kt | 13 +- .../fields/SendAmountFieldChangeConverter.kt | 7 +- .../ui/fee/SendSpeedAndFeeContent.kt | 49 ++-- .../presentation/ui/fee/SendSpeedSelector.kt | 150 +++++++----- .../viewmodel/SendClickIntents.kt | 2 + .../presentation/viewmodel/SendViewModel.kt | 89 +++---- 15 files changed, 465 insertions(+), 339 deletions(-) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index fdd81bd7d9..456bf93702 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -7,7 +7,6 @@ import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,11 +19,8 @@ internal object TransactionDomainModule { @Provides @ViewModelScoped - fun provideGetFeeUseCase( - walletManagersFacade: WalletManagersFacade, - dispatchers: CoroutineDispatcherProvider, - ): GetFeeUseCase { - return GetFeeUseCase(walletManagersFacade, dispatchers) + fun provideGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetFeeUseCase { + return GetFeeUseCase(walletManagersFacade) } @Provides diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt index e27a7d2625..ae697ceb08 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt @@ -1,21 +1,15 @@ package com.tangem.domain.transaction.usecase -import arrow.core.Either -import arrow.core.left -import arrow.core.right +import arrow.core.raise.catch +import arrow.core.raise.either import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn import java.math.BigDecimal /** @@ -23,16 +17,15 @@ import java.math.BigDecimal */ class GetFeeUseCase( private val walletManagersFacade: WalletManagersFacade, - private val dispatcher: CoroutineDispatcherProvider, ) { suspend operator fun invoke( amount: BigDecimal, destination: String, userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Flow> { - return flow { - try { + ) = either { + catch( + block = { val result = requireNotNull( walletManagersFacade.getFee( amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount), @@ -43,14 +36,15 @@ class GetFeeUseCase( ) { "Fee is null" } val maybeFee = when (result) { - is Result.Success -> result.data.right() - is Result.Failure -> GetFeeError.DataError(result.error).left() + is Result.Success -> result.data + is Result.Failure -> raise(GetFeeError.DataError(result.error)) } - emit(maybeFee) - } catch (e: Exception) { - emit(GetFeeError.DataError(e.cause).left()) - } - }.flowOn(dispatcher.io) + maybeFee + }, + catch = { + raise(GetFeeError.DataError(it)) + }, + ) } private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount( 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 4fea850423..185a62b42a 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 @@ -3,11 +3,9 @@ package com.tangem.features.send.impl.presentation.state import androidx.paging.PagingData import arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.event.consumedEvent 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 @@ -17,22 +15,19 @@ import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase 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.* +import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter 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.recipient.SendRecipientListConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider -import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import timber.log.Timber -import java.math.BigDecimal -@Suppress("LongParameterList", "LargeClass") +@Suppress("LongParameterList") internal class SendStateFactory( private val clickIntents: SendClickIntents, private val currentStateProvider: Provider, @@ -41,24 +36,11 @@ internal class SendStateFactory( private val cryptoCurrencyStatusProvider: Provider, private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, - coinCryptoCurrencyStatusProvider: Provider, ) { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) } private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) } - private val customFeeFieldConverter by lazy { - SendFeeCustomFieldConverter( - clickIntents = clickIntents, - appCurrencyProvider = appCurrencyProvider, - ) - } - - private val feeNotificationFactory = FeeNotificationFactory( - coinCryptoCurrencyStatusProvider = coinCryptoCurrencyStatusProvider, - userWalletProvider = userWalletProvider, - clickIntents = clickIntents, - ) private val amountStateConverter by lazy { SendAmountStateConverter( @@ -219,144 +201,6 @@ internal class SendStateFactory( } //endregion - //region fee - 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): SendUiState { - val state = currentStateProvider() - val feeState = state.feeState ?: return state - val feeSelectorState = FeeSelectorState.Content( - fees = fees, - customValues = customFeeFieldConverter.convert(fees.normal), - ) - - val fee = feeSelectorState.getFee() - val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract) - val updatedState = feeState.copy( - feeSelectorState = feeSelectorState, - fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - ) - 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 fee = updatedFeeSelectorState.getFee() - val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract) - - val updatedState = feeState.copy( - fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - 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 fee = updatedFeeSelectorState.getFee() - val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract) - - val updatedState = feeState.copy( - feeSelectorState = updatedFeeSelectorState, - fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - 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 fee = feeSelectorState.getFee() - val receivedAmount = calculateReceiveAmount(state, fee, value) - val updatedState = feeState.copy( - isSubtract = value, - fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = if (value) { - getFormattedValue(receivedAmount) - } 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 customValue = customValues.firstOrNull()?.value?.toBigDecimalOrNull() - val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO - val fee = getFee().amount.value ?: BigDecimal.ZERO - - val isNotEmptyCustom = !customValue.isNullOrZero() && selectedFee == FeeType.CUSTOM - val isNotCustom = selectedFee != FeeType.CUSTOM - fee < balance && (isNotEmptyCustom || isNotCustom) - } - } - } - - private fun getFormattedValue(value: BigDecimal): String { - val cryptoCurrency = cryptoCurrencyStatusProvider().currency - return BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = value, - cryptoCurrency = cryptoCurrency.symbol, - decimals = cryptoCurrency.decimals, - ) - } - //endregion - //region send fun getSendingStateUpdate(isSending: Boolean): SendUiState { val state = currentStateProvider() @@ -376,6 +220,7 @@ internal class SendStateFactory( transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(), isSuccess = true, txUrl = txUrl, + notifications = persistentListOf(), ), ) } 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 46526153e8..f02b6ddf13 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 @@ -54,6 +54,7 @@ internal sealed class SendStates { val isFiatValue: Boolean, val segmentedButtonConfig: PersistentList, val amountTextField: SendTextField.Amount, + val amountValue: BigDecimal, ) : SendStates() /** Recipient state */ @@ -74,6 +75,7 @@ internal sealed class SendStates { val cryptoCurrencyStatus: CryptoCurrencyStatus, val feeSelectorState: FeeSelectorState = FeeSelectorState.Loading, val isSubtract: Boolean = false, + val isUserSubtracted: Boolean = false, val fee: Fee? = null, val receivedAmountValue: BigDecimal = BigDecimal.ZERO, val receivedAmount: String = "", diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt index c80815c345..3420a483f4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt @@ -12,6 +12,7 @@ import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldCo import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal internal class SendAmountStateConverter( private val appCurrencyProvider: Provider, @@ -37,6 +38,7 @@ internal class SendAmountStateConverter( amountTextField = sendAmountFieldConverter.convert(Unit), isFiatValue = false, isPrimaryButtonEnabled = false, + amountValue = BigDecimal.ZERO, segmentedButtonConfig = persistentListOf( SendAmountSegmentedButtonsConfig( title = stringReference(status.currency.symbol), 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 index 1805ce3f6a..de65f8a987 100644 --- 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 @@ -8,14 +8,10 @@ import java.math.BigDecimal /** * Calculate receiving amount when fee is subtracted from sending amount */ -internal fun calculateReceiveAmount(uiState: SendUiState, feeAmount: Fee, isSubtract: Boolean): BigDecimal { - val amount = uiState.amountState?.amountTextField?.value ?: return BigDecimal.ZERO +internal fun calculateReceiveAmount(state: SendUiState, feeAmount: Fee): BigDecimal { + val amountValue = state.amountState?.amountValue ?: BigDecimal.ZERO val fee = feeAmount.amount.value ?: return BigDecimal.ZERO - return if (isSubtract) { - amount.toBigDecimal().minus(fee) - } else { - amount.toBigDecimal() - } + return amountValue.minus(fee) } /** 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 index fa63899356..8f51e6164a 100644 --- 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 @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.state.fee +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.extensions.networkIconResId import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -9,7 +10,6 @@ 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 @@ -19,17 +19,28 @@ internal class FeeNotificationFactory( 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) + operator fun invoke(feeState: SendStates.FeeState, amountValue: BigDecimal?): ImmutableList = + buildList { + when (val feeSelectorState = feeState.feeSelectorState) { + FeeSelectorState.Loading -> Unit + FeeSelectorState.Error -> { + addFeeUnreachableNotification(feeSelectorState) + } + is FeeSelectorState.Content -> { + val customFee = feeSelectorState.customValues + val selectedFee = feeSelectorState.selectedFee + addTooLowNotification(feeSelectorState.fees, selectedFee, customFee) + addTooHighNotification(feeSelectorState.fees, selectedFee, customFee) + addFeeCoverageNotification(feeState, amountValue) + addExceedsBalanceNotification(feeState.fee) + } + } }.toImmutableList() + + private fun MutableList.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) { + if (feeSelectorState is FeeSelectorState.Error) { + add(SendFeeNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload)) + } } private fun MutableList.addTooLowNotification( @@ -59,25 +70,24 @@ internal class FeeNotificationFactory( } } - private fun MutableList.addFeeCoverageNotification() { - // TODO add fee coverage condition [REDACTED_JIRA] - add(SendFeeNotification.Warning.NetworkCoverage) + private fun MutableList.addFeeCoverageNotification( + feeState: SendStates.FeeState, + amountValue: BigDecimal?, + ) { + val cryptoAmount = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO + val value = amountValue ?: BigDecimal.ZERO + if (cryptoAmount <= value + feeValue && feeState.isSubtract && !feeState.isUserSubtracted) { + add(SendFeeNotification.Warning.NetworkCoverage) + } } - private fun MutableList.addExceedsBalanceNotification( - feeSelectorState: FeeSelectorState.Content, - ) { + private fun MutableList.addExceedsBalanceNotification(fee: Fee?) { val coinCryptoCurrency = coinCryptoCurrencyStatusProvider() val cryptoAmount = coinCryptoCurrency.value.amount ?: BigDecimal.ZERO - 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 + val feeValue = fee?.amount?.value ?: BigDecimal.ZERO - if (fee > cryptoAmount) { + if (feeValue > cryptoAmount) { add( SendFeeNotification.Error.ExceedsBalance( coinCryptoCurrency.currency.networkIconResId, 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 c5a392c4a9..9751e61eae 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 @@ -16,6 +16,8 @@ internal sealed class FeeSelectorState { val selectedFee: FeeType = FeeType.MARKET, val customValues: ImmutableList = persistentListOf(), ) : FeeSelectorState() + + object Error : FeeSelectorState() } enum class FeeType { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt new file mode 100644 index 0000000000..abeea708ac --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -0,0 +1,217 @@ +package com.tangem.features.send.impl.presentation.state.fee + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Provider +import com.tangem.utils.isNullOrZero +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal + +/** + * Factory to produce fee state for [SendUiState] + */ +internal class FeeStateFactory( + private val clickIntents: SendClickIntents, + private val currentStateProvider: Provider, + private val coinCryptoCurrencyStatusProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + userWalletProvider: Provider, +) { + private val customFeeFieldConverter by lazy { + SendFeeCustomFieldConverter( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + ) + } + + private val feeNotificationFactory = FeeNotificationFactory( + coinCryptoCurrencyStatusProvider = coinCryptoCurrencyStatusProvider, + userWalletProvider = userWalletProvider, + clickIntents = clickIntents, + ) + + fun onFeeOnLoadingState(): SendUiState { + val state = currentStateProvider() + val feeState = state.feeState ?: return state + return state.copy( + feeState = feeState.copy( + feeSelectorState = FeeSelectorState.Loading, + notifications = persistentListOf(), + isPrimaryButtonEnabled = feeState.isPrimaryButtonEnabled(), + ), + ) + } + + fun onFeeOnLoadedState(fees: TransactionFee): SendUiState { + val state = currentStateProvider() + val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val feeState = state.feeState ?: return state + val feeSelectorState = FeeSelectorState.Content( + fees = fees, + customValues = customFeeFieldConverter.convert(fees.normal), + ) + + val fee = feeSelectorState.getFee() + val receivedAmount = calculateReceiveAmount(state, fee) + val updatedState = feeState.copy( + feeSelectorState = feeSelectorState, + fee = fee, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = checkAutoSubtract(state, fee, balance), + ) + return state.copy( + feeState = updatedState.copy( + notifications = feeNotificationFactory( + feeState = updatedState, + amountValue = state.amountState?.amountValue, + ), + isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), + ), + ) + } + + fun onFeeOnErrorState(): SendUiState { + val state = currentStateProvider() + val updatedState = state.feeState?.copy( + feeSelectorState = FeeSelectorState.Error, + ) + return state.copy( + feeState = updatedState?.copy( + notifications = feeNotificationFactory( + feeState = updatedState, + amountValue = state.amountState?.amountValue, + ), + ), + ) + } + + fun onFeeSelectedState(feeType: FeeType): SendUiState { + val state = currentStateProvider() + val feeState = state.feeState ?: return state + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state + val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + + val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType) + val fee = updatedFeeSelectorState.getFee() + val receivedAmount = calculateReceiveAmount(state, fee) + val updatedState = feeState.copy( + fee = fee, + feeSelectorState = updatedFeeSelectorState, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = checkAutoSubtract(state, fee, balance), + ) + + return state.copy( + feeState = updatedState.copy( + notifications = feeNotificationFactory( + feeState = updatedState, + amountValue = state.amountState?.amountValue, + ), + isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), + ), + ) + } + + 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 balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + + val fee = updatedFeeSelectorState.getFee() + val receivedAmount = calculateReceiveAmount(state, fee) + val updatedState = feeState.copy( + feeSelectorState = updatedFeeSelectorState, + fee = fee, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = checkAutoSubtract(state, fee, balance), + ) + return state.copy( + feeState = updatedState.copy( + notifications = feeNotificationFactory( + feeState = updatedState, + amountValue = state.amountState?.amountValue, + ), + isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), + ), + ) + } + + fun onSubtractSelect(value: Boolean): SendUiState { + val state = currentStateProvider() + val feeState = state.feeState ?: return state + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state + val fee = feeSelectorState.getFee() + val receivedAmount = calculateReceiveAmount(state, fee) + val updatedState = feeState.copy( + isSubtract = value, + isUserSubtracted = true, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + fee = fee, + ) + return state.copy( + feeState = updatedState.copy( + notifications = feeNotificationFactory( + feeState = updatedState, + amountValue = state.amountState?.amountValue, + ), + isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), + ), + ) + } + + private fun SendStates.FeeState.isPrimaryButtonEnabled(): Boolean { + return when (feeSelectorState) { + is FeeSelectorState.Content -> { + val customValue = feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull() + val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val fee = feeSelectorState.getFee().amount.value ?: BigDecimal.ZERO + + val isNotCustom = feeSelectorState.selectedFee != FeeType.CUSTOM + val isNotEmptyCustom = !customValue.isNullOrZero() && !isNotCustom + val isSubtractRequired = if (fee + receivedAmountValue >= balance) isSubtract else true + val isBalanceEnough = fee + receivedAmountValue <= balance + isBalanceEnough && isSubtractRequired && (isNotEmptyCustom || isNotCustom) + } + else -> false + } + } + + private fun checkAutoSubtract(state: SendUiState, fee: Fee, balance: BigDecimal): Boolean { + val feeState = state.feeState ?: return false + val amountValue = state.amountState?.amountValue ?: BigDecimal.ZERO + val feeAmount = fee.amount.value ?: BigDecimal.ZERO + return if (feeState.isUserSubtracted) { + feeState.isSubtract + } else { + amountValue + feeAmount >= balance + } + } + + private fun getFormattedValue(value: BigDecimal): String { + val cryptoCurrency = cryptoCurrencyStatusProvider().currency + return BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = value, + cryptoCurrency = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + ) + } +} \ No newline at end of file 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 index 8763cf5cc2..fbf72931af 100644 --- 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 @@ -1,13 +1,11 @@ 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( @@ -29,11 +27,13 @@ sealed class SendFeeNotification(val config: NotificationConfig) { sealed class Warning( val title: TextReference, val subtitle: TextReference, + val buttonsState: NotificationConfig.ButtonsState? = null, ) : SendFeeNotification( config = NotificationConfig( title = title, subtitle = subtitle, iconResId = R.drawable.img_attention_20, + buttonsState = buttonsState, ), ) { data class TooHigh( @@ -47,6 +47,15 @@ sealed class SendFeeNotification(val config: NotificationConfig) { title = resourceReference(id = R.string.send_network_fee_warning_title), subtitle = resourceReference(id = R.string.send_network_fee_warning_content), ) + + data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning( + title = resourceReference(R.string.send_fee_unreachable_error_title), + subtitle = resourceReference(R.string.send_fee_unreachable_error_text), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_button_refresh), + onClick = onRefresh, + ), + ) } sealed class Error( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt index 66f31d9233..b21b9540ac 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.state.fields +import com.tangem.blockchain.extensions.toBigDecimalOrDefault import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.SendUiState @@ -41,8 +42,9 @@ internal class SendAmountFieldChangeConverter( trimmedValue } - val isExceedBalance = cryptoValue.checkExceedBalance(amountState) - val isMaxAmount = cryptoValue.checkMaxAmount(amountState) + val checkValue = if (amountState.isFiatValue) fiatValue else cryptoValue + val isExceedBalance = checkValue.checkExceedBalance(amountState) + val isMaxAmount = checkValue.checkMaxAmount(amountState) return state.copy( amountState = amountState.copy( isPrimaryButtonEnabled = !isExceedBalance, @@ -51,6 +53,7 @@ internal class SendAmountFieldChangeConverter( fiatValue = fiatValue, isError = isExceedBalance, ), + amountValue = cryptoValue.toBigDecimalOrDefault(), ), feeState = feeState.copy( isSubtract = isMaxAmount, 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 4c8c06116d..2bd84ea7ba 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,7 +3,6 @@ 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.Arrangement import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -16,7 +15,6 @@ 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 @@ -36,7 +34,6 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S .padding( horizontal = TangemTheme.dimens.spacing16, ), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { item( key = FEE_SELECTOR_KEY, @@ -46,13 +43,12 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S clickIntents = clickIntents, ) } - notifications(notifications) customFee( feeSendState = feeSendState, cryptoCurrencySymbol = state.cryptoCurrencyStatus.currency.symbol, ) + notifications(notifications) subtractButton( - feeSendState = feeSendState, receivedAmount = state.receivedAmount, isSubtract = state.isSubtract, clickIntents = clickIntents, @@ -69,8 +65,15 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.background.primary + else -> TangemTheme.colors.button.disabled + }, iconTint = when (it) { is SendFeeNotification.Informational -> TangemTheme.colors.icon.accent else -> null @@ -102,6 +105,7 @@ internal fun LazyListScope.customFee( customValues = customValues, selectedFee = fee.selectedFee, symbol = cryptoCurrencySymbol, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), ) } } @@ -110,32 +114,19 @@ internal fun LazyListScope.customFee( @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 selectedFeeValue = state.selectedFee - val topPadding = if (selectedFeeValue != FeeType.CUSTOM) { - TangemTheme.dimens.spacing8 - } else { - TangemTheme.dimens.spacing0 - } - - SendSpeedSubtract( - receivingAmount = receivedAmount, - isSubtract = isSubtract, - onSelectClick = clickIntents::onSubtractSelect, - modifier = modifier - .animateItemPlacement() - .padding( - top = topPadding, - bottom = TangemTheme.dimens.spacing12, - ), - ) - } + item { + SendSpeedSubtract( + receivingAmount = receivedAmount, + isSubtract = isSubtract, + onSelectClick = clickIntents::onSubtractSelect, + modifier = modifier + .padding(vertical = TangemTheme.dimens.spacing12) + .animateItemPlacement(), + ) } } \ No newline at end of file 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 f221817a1d..db6be28780 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 @@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation.ui.fee import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -13,6 +14,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -26,12 +28,19 @@ import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.R 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.ui.common.FooterContainer import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +private val DEFAULT_FEE_OPTIONS = listOf( + R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24, + R.string.common_fee_selector_option_market to R.drawable.ic_bird_24, + R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24, +) + @Suppress("LongMethod") @Composable internal fun SendSpeedSelector( @@ -50,10 +59,11 @@ internal fun SendSpeedSelector( .background(TangemTheme.colors.background.action), ) { when (state) { + FeeSelectorState.Error -> { + SendSpeedSelectorItemError() + } FeeSelectorState.Loading -> { SendSpeedSelectorItemLoading() - SendSpeedSelectorItemLoading() - SendSpeedSelectorItemLoading() } is FeeSelectorState.Content -> { when (state.fees) { @@ -84,7 +94,10 @@ internal fun SendSpeedSelector( onSelect = { clickIntents.onFeeSelectorClick(FeeType.FAST) }, showDivider = state.fees.normal is Fee.Ethereum, ) - if (state.fees.normal is Fee.Ethereum) { + AnimatedVisibility( + visible = state.fees.normal is Fee.Ethereum, + label = "Custom fee appearance animation", + ) { SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_custom, iconRes = R.drawable.ic_edit_24, @@ -114,34 +127,52 @@ internal fun SendSpeedSelector( @Composable private fun SendSpeedSelectorItemLoading() { - Row(modifier = Modifier.fillMaxWidth()) { - RectangleShimmer( - radius = TangemTheme.dimens.radius3, - modifier = Modifier - .padding( - top = TangemTheme.dimens.spacing18, - bottom = TangemTheme.dimens.spacing18, - start = TangemTheme.dimens.spacing12, - ) - .size( - width = TangemTheme.dimens.size50, - height = TangemTheme.dimens.size12, - ), - ) - SpacerWMax() - RectangleShimmer( - radius = TangemTheme.dimens.radius3, - modifier = Modifier - .padding( - top = TangemTheme.dimens.spacing18, - bottom = TangemTheme.dimens.spacing18, - end = TangemTheme.dimens.spacing12, - ) - .size( - width = TangemTheme.dimens.size90, - height = TangemTheme.dimens.size12, - ), - ) + repeat(DEFAULT_FEE_OPTIONS.size) { + val (text, iconRes) = DEFAULT_FEE_OPTIONS[it] + Row(modifier = Modifier.fillMaxWidth()) { + SelectorTitleContent( + titleRes = text, + iconRes = iconRes, + ) + SpacerWMax() + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing18, + bottom = TangemTheme.dimens.spacing18, + end = TangemTheme.dimens.spacing12, + ) + .size( + width = TangemTheme.dimens.size90, + height = TangemTheme.dimens.size12, + ), + ) + } + } +} + +@Composable +private fun SendSpeedSelectorItemError() { + repeat(DEFAULT_FEE_OPTIONS.size) { + val (text, iconRes) = DEFAULT_FEE_OPTIONS[it] + Row(modifier = Modifier.fillMaxWidth()) { + SelectorTitleContent( + titleRes = text, + iconRes = iconRes, + ) + SpacerWMax() + Text( + text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + vertical = TangemTheme.dimens.spacing14, + horizontal = TangemTheme.dimens.spacing12, + ), + ) + } } } @@ -177,27 +208,11 @@ private fun SendSpeedSelectorItem( .clickable { onSelect() }, ) { Row(modifier = Modifier.fillMaxWidth()) { - Icon( - painter = painterResource(iconRes), - tint = iconTint, - contentDescription = null, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ), - ) - Text( - text = stringResource(titleRes), - style = textStyle, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing8, - top = TangemTheme.dimens.spacing14, - bottom = TangemTheme.dimens.spacing14, - ), + SelectorTitleContent( + titleRes = titleRes, + iconRes = iconRes, + iconTint = iconTint, + textStyle = textStyle, ) if (amount != null && symbol != null) { SelectorValueContent( @@ -220,6 +235,37 @@ private fun SendSpeedSelectorItem( } } +@Composable +private fun SelectorTitleContent( + @StringRes titleRes: Int, + @DrawableRes iconRes: Int, + iconTint: Color = TangemTheme.colors.icon.informative, + textStyle: TextStyle = TangemTheme.typography.body2, +) { + Icon( + painter = painterResource(iconRes), + tint = iconTint, + contentDescription = null, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + ) + Text( + text = stringResource(titleRes), + style = textStyle, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing8, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing14, + ), + ) +} + @Composable private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextReference, textStyle: TextStyle) { Text( 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 90e3049f47..cd3c448c3f 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 @@ -36,6 +36,8 @@ interface SendClickIntents { // endregion // region Fee + fun feeReload() + fun onFeeSelectorClick(feeType: FeeType) fun onCustomFeeValueChange(index: Int, 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 151eea4275..7b406ca2d1 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 @@ -35,6 +35,7 @@ import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.* import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.fee.getFee import com.tangem.utils.Provider @@ -93,11 +94,21 @@ internal class SendViewModel @Inject constructor( userWalletProvider = Provider { userWallet }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, validateWalletMemoUseCase = validateWalletMemoUseCase, getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase, ) + private val feeStateFactory by lazy { + FeeStateFactory( + clickIntents = this, + currentStateProvider = Provider { uiState }, + coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + userWalletProvider = Provider { userWallet }, + ) + } + private val eventStateFactory = SendEventStateFactory( clickIntents = this, currentStateProvider = Provider { uiState }, @@ -129,7 +140,7 @@ internal class SendViewModel @Inject constructor( override fun onCreate(owner: LifecycleOwner) { subscribeOnCurrencyStatusUpdates(owner) - getFee() + onFeeStateActive() } fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) { @@ -289,37 +300,11 @@ internal class SendViewModel @Inject constructor( } } - private fun getFee() { - viewModelScope.launch(dispatchers.main) { - uiState.currentState - .filter { it == SendUiStateType.Fee } - .onEach { - val amountState = uiState.amountState ?: return@onEach - val recipientState = uiState.recipientState ?: return@onEach - val amount = amountState.amountTextField.value.toBigDecimal() - - uiState = stateFactory.onFeeOnLoadingState() - getFeeUseCase.invoke( - amount = amount, - destination = recipientState.addressTextField.value, - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrency, - ) - .conflate() - .distinctUntilChanged() - .onEach { maybeFee -> - maybeFee.fold( - ifRight = { - uiState = stateFactory.onFeeOnLoadedState(it) - }, - ifLeft = { - // todo add error handling [[REDACTED_JIRA]] - }, - ) - } - .launchIn(viewModelScope) - }.launchIn(viewModelScope) - }.saveIn(feeJobHolder) + private fun onFeeStateActive() { + uiState.currentState + .filter { it == SendUiStateType.Fee } + .onEach { loadFee() } + .launchIn(viewModelScope) } private fun updateNotifications() { @@ -428,16 +413,41 @@ internal class SendViewModel @Inject constructor( // endregion // region fee + override fun feeReload() = loadFee() + override fun onFeeSelectorClick(feeType: FeeType) { - uiState = stateFactory.onFeeSelectedState(feeType) + uiState = feeStateFactory.onFeeSelectedState(feeType) } override fun onCustomFeeValueChange(index: Int, value: String) { - uiState = stateFactory.onCustomFeeValueChange(index, value) + uiState = feeStateFactory.onCustomFeeValueChange(index, value) } override fun onSubtractSelect(value: Boolean) { - uiState = stateFactory.onSubtractSelect(value) + uiState = feeStateFactory.onSubtractSelect(value) + } + + private fun loadFee() { + viewModelScope.launch(dispatchers.main) { + val amountState = uiState.amountState ?: return@launch + val recipientState = uiState.recipientState ?: return@launch + val amount = amountState.amountTextField.value.toBigDecimal() + + uiState = feeStateFactory.onFeeOnLoadingState() + getFeeUseCase.invoke( + amount = amount, + destination = recipientState.addressTextField.value, + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ).fold( + ifRight = { + uiState = feeStateFactory.onFeeOnLoadedState(it) + }, + ifLeft = { + uiState = feeStateFactory.onFeeOnErrorState() + }, + ) + }.saveIn(feeJobHolder) } // endregion @@ -461,7 +471,7 @@ internal class SendViewModel @Inject constructor( override fun onAmountReduceClick(reducedAmount: String) { uiState = stateFactory.getOnAmountValueChange(reducedAmount) uiState = sendNotificationFactory.dismissHighFeeWarningState() - getFee() + loadFee() } override fun onAmountReduceIgnoreClick() { @@ -474,12 +484,13 @@ internal class SendViewModel @Inject constructor( val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return val memo = uiState.recipientState?.memoTextField?.value val fee = feeSelectorState.getFee() + val amountValue = uiState.amountState?.amountValue ?: return - val amountToSend = feeState.receivedAmountValue.convertToAmount(cryptoCurrency) + val amountToSend = if (feeState.isSubtract) feeState.receivedAmountValue else amountValue viewModelScope.launch(dispatchers.main) { createTransactionUseCase( - amount = amountToSend, + amount = amountToSend.convertToAmount(cryptoCurrency), fee = fee, memo = memo, destination = recipient,