From 0b037f2b2da7d7da9d7d9233eb583ae80babb1e5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Jan 2024 16:20:14 +0300 Subject: [PATCH 01/15] 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 02/15] 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 03/15] 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 04/15] 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, From 5efe33a6b616ff9d844cf3428447b5f4bbb78fdb Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Jan 2024 14:47:18 +0300 Subject: [PATCH 05/15] Updated on 2026-08-14 --- .../java/com/tangem/feature/swap/models/SwapStateHolder.kt | 1 + .../main/java/com/tangem/feature/swap/ui/StateBuilder.kt | 7 +++++++ .../java/com/tangem/feature/swap/ui/SwapScreenContent.kt | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index bbe757b01b..4e77ff7dc8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -31,6 +31,7 @@ data class SwapStateHolder( val bottomSheetConfig: TangemBottomSheetConfig? = null, val swapButton: SwapButton, + val shouldShowMaxAmount: Boolean, val tosState: TosState? = null, val onRefresh: () -> Unit, 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 90e2f34c73..76f84dc0e2 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 @@ -87,6 +87,7 @@ internal class StateBuilder( changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, onShowPermissionBottomSheet = actions.openPermissionBottomSheet, providerState = ProviderState.Empty(), + shouldShowMaxAmount = false, priceImpact = PriceImpact.Empty(), ) } @@ -187,6 +188,7 @@ internal class StateBuilder( permissionState = uiStateHolder.permissionState, changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, priceImpact = PriceImpact.Empty(), + shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toToken), ) } @@ -276,9 +278,14 @@ internal class StateBuilder( PriceImpact.Empty() }, tosState = createTosState(swapProvider), + shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toCurrencyStatus.currency), ) } + private fun shouldShowMaxAmount(fromToken: CryptoCurrency, toCurrency: CryptoCurrency): Boolean { + return !(fromToken is CryptoCurrency.Coin && fromToken.network.id == toCurrency.network.id) + } + private fun createTosState(swapProvider: SwapProvider): TosState { return TosState( tosLink = swapProvider.termsOfUse?.let { 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 3b42cf78a6..8542161c27 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 @@ -86,7 +86,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi } } - if (keyboard is Keyboard.Opened) { + if (state.shouldShowMaxAmount && keyboard is Keyboard.Opened) { Text( text = stringResource(id = R.string.send_max_amount_label), style = TangemTheme.typography.button, @@ -494,6 +494,7 @@ private val state = SwapStateHolder( blockchainId = "POLYGON", providerState = ProviderState.Loading(), priceImpact = PriceImpact.Empty(), + shouldShowMaxAmount = true, tosState = TosState( tosLink = LegalState( title = stringReference("Terms of Use"), From e4c91780ed8e645f83dd6813fef62541708cacf4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Jan 2024 12:53:17 +0300 Subject: [PATCH 06/15] Updated on 2026-08-14 --- .../tangem/domain/common/CardTypesResolver.kt | 6 +++++ .../domain/common/TangemCardTypesResolver.kt | 9 +++++++ .../wallet/domain/WalletImageResolver.kt | 24 ++++++++++++++++++ .../res/drawable/ill_grim_card2_120_106.webp | Bin 0 -> 35242 bytes .../res/drawable/ill_grim_card3_120_106.webp | Bin 0 -> 46302 bytes .../res/drawable/ill_jr_card2_120_106.webp | Bin 0 -> 29208 bytes .../res/drawable/ill_jr_card3_120_106.webp | Bin 0 -> 39188 bytes .../drawable/ill_satoshi_card2_120_106.webp | Bin 0 -> 48698 bytes .../drawable/ill_satoshi_card3_120_106.webp | Bin 0 -> 46866 bytes 9 files changed, 39 insertions(+) create mode 100644 features/wallet/impl/src/main/res/drawable/ill_grim_card2_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_grim_card3_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_jr_card2_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_jr_card3_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_satoshi_card2_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_satoshi_card3_120_106.webp diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index f4376a8ef5..0cbee25c28 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -24,6 +24,12 @@ interface CardTypesResolver { fun isBadWallet(): Boolean + fun isJrWallet(): Boolean + + fun isGrimWallet(): Boolean + + fun isSatoshiFriendsWallet(): Boolean + fun isWhiteWallet(): Boolean fun isWallet2(): Boolean diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index f05317c4e6..db6be16a4d 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -42,6 +42,12 @@ internal class TangemCardTypesResolver( override fun isBadWallet(): Boolean = card.batchId == BAD_WALLET_BATCH_ID + override fun isJrWallet(): Boolean = card.batchId == JR_WALLET_BATCH_ID + + override fun isGrimWallet(): Boolean = card.batchId == GRIM_WALLET_BATCH_ID + + override fun isSatoshiFriendsWallet(): Boolean = card.batchId == SATOSHI_WALLET_BATCH_ID + override fun isWhiteWallet(): Boolean { return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } @@ -138,6 +144,9 @@ internal class TangemCardTypesResolver( const val TRON_WALLET_BATCH_ID = "AF07" const val KASPA_WALLET_BATCH_ID = "AF08" const val BAD_WALLET_BATCH_ID = "AF09" + const val JR_WALLET_BATCH_ID = "AF14" + const val GRIM_WALLET_BATCH_ID = "AF13" + const val SATOSHI_WALLET_BATCH_ID = "AF19" const val WHITE_WALLET2_BATCH_ID = "AF15" const val TRILLIANT_WALLET_BATCH_ID = "AF16" const val AVRORA_WALLET_BATCH_ID = "AF18" diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index 963970f85a..7c7adbde20 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -31,6 +31,9 @@ internal object WalletImageResolver { cardTypesResolver.isTronWallet() -> userWallet.resolveTronWallet() cardTypesResolver.isKaspaWallet() -> userWallet.resolveKaspaWallet() cardTypesResolver.isBadWallet() -> userWallet.resolveBadWallet() + cardTypesResolver.isJrWallet() -> userWallet.resolveJrWallet() + cardTypesResolver.isGrimWallet() -> userWallet.resolveGrimWallet() + cardTypesResolver.isSatoshiFriendsWallet() -> userWallet.resolveSatoshiWallet() cardTypesResolver.isWallet2() -> userWallet.resolveWallet2() cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet() cardTypesResolver.isTangemWallet() -> userWallet.resolveWallet1() @@ -78,6 +81,27 @@ internal object WalletImageResolver { ) } + private fun UserWallet.resolveJrWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_jr_card2_120_106, + twoBackupResId = R.drawable.ill_jr_card3_120_106, + ) + } + + private fun UserWallet.resolveGrimWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_grim_card2_120_106, + twoBackupResId = R.drawable.ill_grim_card3_120_106, + ) + } + + private fun UserWallet.resolveSatoshiWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_satoshi_card2_120_106, + twoBackupResId = R.drawable.ill_satoshi_card3_120_106, + ) + } + private fun UserWallet.resolveShibaWallet(): Int? { return resolveWallet2( oneBackupResId = R.drawable.ill_shiba_card2_120_106, diff --git a/features/wallet/impl/src/main/res/drawable/ill_grim_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_grim_card2_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..5568ac6e06feda92e30461533dc693a2e4df4126 GIT binary patch literal 35242 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 literal 0 HcmV?d00001 diff --git a/features/wallet/impl/src/main/res/drawable/ill_grim_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_grim_card3_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..f58a7467c02457bfc2fc6cafebe784594cc4efd6 GIT binary patch literal 46302 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�i+~L&GBcK04i16{#wU7~lq8V@i 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
M7<29?n)5xu5tm;=o!My2*=M`N*-9o2 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 ANdN!< literal 0 HcmV?d00001 diff --git a/features/wallet/impl/src/main/res/drawable/ill_satoshi_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_satoshi_card2_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..31310554b3d0641d78cad95458fbf6ca9e5781a3 GIT binary patch literal 48698 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≪xsz?Wm3N*ir8sjBcCmlLI1Fiz7ln`CNIK-AK&yB@cj*$Dlc z#g!nrw)r$?Tt_G9ubs!^&g%?WxW~qi{!!dZH`|k1>TcT1a~&v@0+8GO_CNM_-oN@# ze%?Tqxvk_yg@S}GUS&(VY%Dw=vFsh6yT^OZF5ZKeXgurPJjb&fRqwLMLMD_go2$+2 zl^{f%K+jAYy_*Kc39~^u)p6Kg!C0^m5??OkU+dM&<{0&YLpOF8H+6b9etq^+>`xU` zJu^ttmLjU6o;ylOp>DUfjb%)Th+RSnP2+a6CPu`qs(tR@klSra4-a0J|79Ngn$w!t zyO!0EmVWY?_u)FOGh8zRN~&h0-yAeo?)lx_-?-=3Y!^r4(r1G-sBO~f=1Mxb4;~YT zGt4Q*8g;bVpbn#d?J||LPxO(~cs|Fdz4sp2E3-mT$8sw5*|a9FaWY$*+yWBEvEf1y zr5n%DQ@{B`DC$hAs${LzDb&-PXYV}EbgrvL^E^iks;XC2r8~zMN7Xla57bb)2hU?? zp)eLj>N%zZ1E1wM7>=e&V%Rh6dOJF0g+=ci&{fp{VBAbzwHUWv9J8Kw%FLOqQFryViC@3<4_B>AcyK z(=*VlH`!W3x#-HnVF}8P6!jzciuCR?FAF(sv>oR~#|9x_9qKqQtgoLr{$Oh4#3+RU zNG4$aYzh(E=6s1NffLYLlQ}_{iuCF{b;r5Ye-3{(U~rp9*cqjgnAlgT4*1qC#Qx$5O28?vJT&tPBU}4a?DPZ83t9A`d-%Wq6y+sbj~$Z@IdPMSQ=yXgYD zDC&YxY8)Ld7d;(9nVya-844xHG;J-JL1sg%+piGb~bJgp<6f(@!KK)$XIq5z#_eKWAHJQX>Jt}6$)yM)-uVQ z+cwpY&+8%^PX*;IOJLNvvdqbG#Eb)_e>#rpoXpRr%35EAF?tEAsuG?~DCKu}52y~# zQ}?i^Te{DoeU3CA8ah4$M`eiJ?h7Lm-~c7uu$}}@5mTRiz1)#zy5nLeh8fD4W9+D< z9rmbjRyxeNjJXgt>NMXk0%nJ$eHx~JqO`ir_OQC+bSF;Eu$w5Ni)6|{Vz$h-H>wGQ zzvANor{#Gz?s;!a#|!;IxN_?s`swW*r_qgJQu=8dVJd0#Koux>pZ2S8npw7{6zKIbWKGBb~PTAQ*9Ml2%*OV{#&% z55KS9T@Q-_5Y!f(%d-Q~Heog*dx_hK`>{-kv*PXB&QYlm!i51PhN^QpHwMO%`Js+u z!(Fm**ISg$yK{S_!P8t<(lP2@z3Qo_ac-zOkhK(*AKr5gXea5M`^(>`1Zl7BvX4}2p|NSZ22eS9(xg%YZx@EnHE z_|@71t#?f-Loee9c33&Q-+GT{xk7}`a^{T0!gw?^cZOL}fCMF-gUAv$=EVSB%oHJr&)twE6ZppGOj*j=PiQ!ih91S<7nDBh46`CkfZ3 ztSbpszhm!#UrD1Hf?|i7H?>32$YjB(SxCeJeP_qv(ufK?c$JD&QV6M|+^g;f3PLv~ zbp?d`pp|$3D~=x4W{x@-&-ecAO6E-HGYOir1}gp~YhWgJ=o~1Obr{i5*vTr<8oFVX z9x}^(r_9-jWxJQMcf;kdPL!7iN;ghpcrkFp7#7yTIZ)o0NkI~ue_B)u!)gkZ4skN0 z>s=;^a5RD4d`$+o8w!$303Iols%B~`WQ*ep;{qID8~{hzrT<;avGeoI+8yH6*Y6+B zeJ-;)ekI^`z63H0wHSz$pOL1SRGM}cx*_QxA~UTwU3!is_N9xbSiUm&VtoV<{3E+0WjhP!Y0LCwEcC*mc)| zG3?xi!u`yv2>{q$;SFtfWZB5A?=j=!=YRdd{i`qg1XSQ0FLGSX>t7@4<%d}R`>{oWYBZlLkCstzTwX5pkRtHsx_u=oz{*oJ6@Os-uSotvg%Maox}Cg=RK7p2Z@R=5P>M zgrZQMg6~UG2LpV+$RLUp1_g*D2nl5jbQN2~4QES~*evoICALd{h3K!| zZFlTmyV}KX4xzjwYonB%6-e1VYFy4ygrzUdV`*|Lp-fN9k1AB5S0=DDG9&; z2(^mvI6fVJZ!X&j&j{PNihqsS7n0Y|i5fA%wNyNh5_lyN>B6{uw@j+E1=XOuVW3nM zsw98QwZ`eqPn*xi>nncy;v*D~@8918-{`+kqMASqOI4r{6v0ZAWp+T5c)l1f`+n$L zf!c#ps3Uxl&!Kz(Mamd)+BcA@`ZoN1%eIBXO>9-)dK=Hv3dIqYJ_;QQ|*?8l`8Or+h7V43gF2zC;n(Lhxy)B0a8zB&6wOzu86>Sg}EKonOxbn_h&TRJ=Q zQ39uSdh=y7y3r5m%*)QDTUaR5W;)plq9RJ(36d(R=MT0|m=0@)Djvjg80JT>!__pN z)Uc(41-KdbWrkwWB!^Ql8~_GF2u?eO&-gS*{OTe$8%cz#*6RB`GYE!NsPBLE9w*h%ELbQpjT&kd|xo?Hx;#f$NT4~*u)S{rK-IJNbwe?Hy=lT=+-mG)9nR8QGn zlql6zSYY$ZXwT^H!(fbP>-!MEJ8(NY&i&isH9Z;{uZtL)?(0cTu}|C+*riUwCpcGs zkJ2p*+Za~Qjp$uBp7~5EuM30jcTwev_wGlpQqSn?jxo~UH&dpe4Bc?{UaYEdsc~&2 zaHC3&%W3`g#l-e;z?vrtk44q6UCnhV7IG`Y%TWS9eq!%|TX*R%2}KkhpXbJA=mmq8 z7`>Yw^n(EckuBGVpL_i-Rz_o+aZg|te~1vlbfOGeC+5&_l*q-CEMGj-R~6k utLbNy>B75rl5W0_C`Nl2`s{2kQ+H8b9WsY*n(jjo+gUrhAO1l&b<+WuWY{SH literal 0 HcmV?d00001 diff --git a/features/wallet/impl/src/main/res/drawable/ill_satoshi_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_satoshi_card3_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..3652d41e727433fc665628751ca703d10b082644 GIT binary patch literal 46866 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 z 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 ifOTclmIJd1Un Date: Wed, 24 Jan 2024 13:13:33 +0300 Subject: [PATCH 07/15] Updated on 2026-08-14 --- .../tangem/utils/coroutines/PeriodicTask.kt | 4 + .../impl/presentation/state/SendAlertState.kt | 5 + .../state/SendEventStateFactory.kt | 41 ++++- .../presentation/state/fee/FeeStateFactory.kt | 18 ++- .../ui/recipient/TextFieldWithPaste.kt | 1 + .../presentation/viewmodel/SendViewModel.kt | 144 ++++++++++++++---- 6 files changed, 175 insertions(+), 38 deletions(-) diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt index 5db470b916..3beae6ac03 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt @@ -10,12 +10,16 @@ class PeriodicTask( private val task: suspend () -> Result, private val onSuccess: (T) -> Unit, private val onError: (Throwable) -> Unit, + private val isDelayFirst: Boolean = false, ) { private var isActive: AtomicBoolean = AtomicBoolean(false) suspend fun runTaskWithDelay() { isActive.set(true) + if (isDelayFirst) { + delay(delay) + } while (isActive.get()) { task.invoke() .onSuccess { 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 index ff2aac684f..54eb2f7868 100644 --- 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 @@ -44,4 +44,9 @@ internal sealed class 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) } + + object FeeIncreased : SendAlertState() { + override val title: TextReference? = null + override val message: TextReference = resourceReference(id = R.string.send_notification_high_fee_title) + } } \ 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 index 8bd869cb7c..794f00706a 100644 --- 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 @@ -1,20 +1,27 @@ package com.tangem.features.send.impl.presentation.state +import com.tangem.blockchain.common.transaction.TransactionFee 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.state.fee.* +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.getFee import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider +import java.math.BigDecimal /** * Factory to produce event state for [SendUiState] * * @param currentStateProvider [Provider] of [SendUiState] * @param clickIntents [SendClickIntents] + * @param feeStateFactory [FeeStateFactory] */ internal class SendEventStateFactory( - val currentStateProvider: Provider, - val clickIntents: SendClickIntents, + private val currentStateProvider: Provider, + private val clickIntents: SendClickIntents, + private val feeStateFactory: FeeStateFactory, ) { private val sendTransactionErrorConverter by lazy { SendTransactionAlertConverter(clickIntents) } @@ -34,6 +41,36 @@ internal class SendEventStateFactory( ) } + fun getFeeUpdatedAlert(fee: TransactionFee, onConsume: () -> Unit): SendUiState { + val state = currentStateProvider() + val feeSelector = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state + val newFee = when (fee) { + is TransactionFee.Single -> fee.normal + is TransactionFee.Choosable -> { + when (feeSelector.selectedFee) { + FeeType.SLOW -> fee.minimum + FeeType.MARKET -> fee.normal + FeeType.FAST -> fee.priority + FeeType.CUSTOM -> return state + } + } + } + + val newFeeValue = newFee.amount.value ?: BigDecimal.ZERO + val oldFeeValue = feeSelector.getFee().amount.value ?: BigDecimal.ZERO + val updateFeeState = feeStateFactory.onFeeOnLoadedState(fee) + return if (newFeeValue > oldFeeValue) { + updateFeeState.copy( + event = triggeredEvent( + data = SendEvent.ShowAlert(SendAlertState.FeeIncreased), + onConsume = onConsume, + ), + ) + } else { + updateFeeState + } + } + fun getGenericErrorState(error: Throwable? = null, onConsume: () -> Unit): SendUiState { val state = currentStateProvider() return state.copy( 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 index abeea708ac..9ad8b30568 100644 --- 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 @@ -55,7 +55,10 @@ internal class FeeStateFactory( val state = currentStateProvider() val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO val feeState = state.feeState ?: return state - val feeSelectorState = FeeSelectorState.Content( + val feeSelectorState = (feeState.feeSelectorState as? FeeSelectorState.Content)?.copy( + fees = fees, + customValues = customFeeFieldConverter.convert(fees.normal), + ) ?: FeeSelectorState.Content( fees = fees, customValues = customFeeFieldConverter.convert(fees.normal), ) @@ -182,14 +185,17 @@ internal class FeeStateFactory( 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 balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val fee = feeSelectorState.getFee() + val feeValue = fee.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) + val isFiatAnotherCurrency = cryptoCurrencyStatusProvider().currency.symbol != fee.amount.currencySymbol + val isSubtractRequired = if (feeValue + receivedAmountValue >= balance) isSubtract else true + val isBalanceEnough = feeValue + receivedAmountValue <= balance + + (isFiatAnotherCurrency || isBalanceEnough && isSubtractRequired) && (isNotEmptyCustom || isNotCustom) } else -> false } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt index 8b3c83bfb0..b01980e9e9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -55,6 +55,7 @@ internal fun TextFieldWithPaste( placeholder = placeholder, onValueChange = onValueChange, modifier = Modifier + .weight(1f) .padding(top = TangemTheme.dimens.spacing6), ) } 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 7b406ca2d1..57d9dae131 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 @@ -5,20 +5,24 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* import androidx.paging.PagingData +import arrow.core.Either 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.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.utils.convertToAmount +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase @@ -39,15 +43,10 @@ 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 -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject import kotlin.properties.Delegates @@ -58,6 +57,7 @@ internal class SendViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, @@ -98,20 +98,19 @@ internal class SendViewModel @Inject constructor( 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 feeStateFactory = 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 }, + feeStateFactory = feeStateFactory, ) private val sendNotificationFactory = SendNotificationFactory( @@ -138,9 +137,17 @@ internal class SendViewModel @Inject constructor( private var sendNotificationsJobHolder = JobHolder() private var qrScannerJobHolder = JobHolder() + private var checkFeeUpdateScheduler = SingleTaskScheduler?>() + override fun onCreate(owner: LifecycleOwner) { subscribeOnCurrencyStatusUpdates(owner) onFeeStateActive() + onCheckFeeUpdate() + } + + override fun onStop(owner: LifecycleOwner) { + super.onStop(owner) + checkFeeUpdateScheduler.cancelTask() } fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) { @@ -429,26 +436,30 @@ internal class SendViewModel @Inject constructor( 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( + uiState = callFeeUseCase()?.fold( ifRight = { - uiState = feeStateFactory.onFeeOnLoadedState(it) + feeStateFactory.onFeeOnLoadedState(it) }, ifLeft = { - uiState = feeStateFactory.onFeeOnErrorState() + feeStateFactory.onFeeOnErrorState() }, - ) + ) ?: feeStateFactory.onFeeOnErrorState() }.saveIn(feeJobHolder) } + + private suspend fun callFeeUseCase(): Either? { + val amountState = uiState.amountState ?: return null + val recipientState = uiState.recipientState ?: return null + val amount = amountState.amountTextField.value.toBigDecimal() + + return getFeeUseCase.invoke( + amount = amount, + destination = recipientState.addressTextField.value, + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ) + } // endregion // region send state clicks @@ -527,13 +538,86 @@ internal class SendViewModel @Inject constructor( ifRight = { uiState = stateFactory.getSendingStateUpdate(isSending = false) uiState = stateFactory.getTransactionSendState(txData) + scheduleBalanceUpdate() }, ) } + + private fun scheduleBalanceUpdate() { + viewModelScope.launch(dispatchers.io) { + delay(BALANCE_UPDATE_DELAY) + fetchCurrencyStatusUseCase.invoke( + userWalletId = userWalletId, + id = cryptoCurrency.id, + refresh = true, + ) + } + } + + private fun onCheckFeeUpdate() { + uiState.currentState + .onEach { activeState -> + val isSending = uiState.sendState.isSending + val isSuccess = uiState.sendState.isSuccess + val isNoNotifications = uiState.sendState.notifications.isEmpty() + val isSendIdle = !isSending && !isSuccess && isNoNotifications + if (activeState == SendUiStateType.Send && isSendIdle) { + checkFeeUpdateScheduler.scheduleTask( + scope = viewModelScope, + task = getFeePeriodicTask(), + ) + } else { + checkFeeUpdateScheduler.cancelTask() + } + } + .launchIn(viewModelScope) + } + + private fun getFeePeriodicTask() = PeriodicTask( + delay = CHECK_FEE_UPDATE_DELAY, + isDelayFirst = true, + task = { + runCatching(dispatchers.main) { + if (uiState.sendState.isSuccess) { + checkFeeUpdateScheduler.cancelTask() + null + } else { + callFeeUseCase() + } + } + }, + onSuccess = { maybeFee -> + if (maybeFee == null) { + checkFeeUpdateScheduler.cancelTask() + uiState = eventStateFactory.getGenericErrorState( + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + } else { + maybeFee.fold( + ifRight = { + uiState = eventStateFactory.getFeeUpdatedAlert( + fee = it, + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + }, + ifLeft = { + checkFeeUpdateScheduler.cancelTask() + uiState = eventStateFactory.getGenericErrorState( + error = (it as? GetFeeError.DataError)?.cause, + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + }, + ) + } + }, + onError = { /* no-op */ }, + ) // endregion companion object { private const val XRP_X_ADDRESS = 'X' private const val DEFAULT_VALUE = "0.00" + private const val CHECK_FEE_UPDATE_DELAY = 60_000L + private const val BALANCE_UPDATE_DELAY = 10_000L } } \ No newline at end of file From 0450ce990664c34667561c9764283664418ecaa1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jan 2024 10:44:28 +0300 Subject: [PATCH 08/15] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../main/java/com/tangem/tap/MainActivity.kt | 14 +- .../tap/proxy/redux/DaggerGraphState.kt | 2 +- core/deep-links/.gitignore | 1 + core/deep-links/build.gradle.kts | 23 +++ .../com/tangem/core/deeplink/DeepLink.kt | 36 ++++ .../tangem/core/deeplink/DeepLinksRegistry.kt | 63 +++++++ .../core/deeplink/di/DeepLinksModule.kt | 20 ++ .../deeplink/impl/DefaultDeepLinksRegistry.kt | 173 ++++++++++++++++++ .../utils/DeepLinksLifecycleObserver.kt | 20 ++ features/send/api/build.gradle.kts | 1 - settings.gradle.kts | 3 +- 12 files changed, 353 insertions(+), 4 deletions(-) create mode 100644 core/deep-links/.gitignore create mode 100644 core/deep-links/build.gradle.kts create mode 100644 core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLink.kt create mode 100644 core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt create mode 100644 core/deep-links/src/main/kotlin/com/tangem/core/deeplink/di/DeepLinksModule.kt create mode 100644 core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt create mode 100644 core/deep-links/src/main/kotlin/com/tangem/core/deeplink/utils/DeepLinksLifecycleObserver.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 85ed2fdfb3..834e2bdd48 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -62,6 +62,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.datasource) implementation(projects.core.utils) + implementation(projects.core.deepLinks) implementation(projects.libs.crypto) implementation(projects.libs.auth) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index c31c62fa66..74f347ee5f 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -23,9 +23,9 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import arrow.core.getOrElse import by.kirich1409.viewbindingdelegate.viewBinding -import com.tangem.feature.qrscanning.QrScanningRouter import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar +import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.ui.event.StateEvent @@ -36,6 +36,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.managetokens.navigation.ManageTokensRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.tester.api.TesterRouter @@ -138,6 +139,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var qrScanningRouter: QrScanningRouter + @Inject + lateinit var deepLinksRegistry: DeepLinksRegistry + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -167,6 +171,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac checkForNotificationPermission() observeStateUpdates() + + if (intent != null) { + deepLinksRegistry.launch(intent) + } } private fun observeStateUpdates() { @@ -332,6 +340,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lifecycleScope.launch { intentProcessor.handleIntent(intent) } + + if (intent != null) { + deepLinksRegistry.launch(intent) + } } override fun showSnackbar( diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 6bd651f70c..b1839fae93 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -1,6 +1,5 @@ package com.tangem.tap.proxy.redux -import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -13,6 +12,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import com.tangem.features.managetokens.navigation.ManageTokensRouter import com.tangem.features.send.api.featuretoggles.SendFeatureToggles diff --git a/core/deep-links/.gitignore b/core/deep-links/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/core/deep-links/.gitignore @@ -0,0 +1 @@ +/build diff --git a/core/deep-links/build.gradle.kts b/core/deep-links/build.gradle.kts new file mode 100644 index 0000000000..9d43ab04df --- /dev/null +++ b/core/deep-links/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.core.deeplink" +} + +dependencies { + + /* Libs - AndroidX */ + implementation(deps.lifecycle.runtime.ktx) + + /* Libs - Other */ + implementation(deps.timber) + + /* DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLink.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLink.kt new file mode 100644 index 0000000000..0b85327f28 --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLink.kt @@ -0,0 +1,36 @@ +package com.tangem.core.deeplink + +/** + * Represents a deep link. + */ +interface DeepLink { + + /** + * ID of the deep link. + * + * By default, it is the same as the [uri]. + * */ + val id: String get() = uri + + /** + * URI of the deep link. + * + * **Note: Remember to add the URI in the AndroidManifest.xml file in the `app` module.** + * + * Query parameters will be received automatically. + * + * Path parameters can be added using the following syntax: + * ```kotlin + * "tangem://link" // Without parameters + * "tangem://link/{param1}/{param2}" // With path parameters + * ``` + * */ + val uri: String + + /** + * Method to be called when this deep link is received. + * + * @param params Map of parameters received from the deep link. + * */ + fun onReceive(params: Map) +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt new file mode 100644 index 0000000000..beaeb5f0d2 --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt @@ -0,0 +1,63 @@ +package com.tangem.core.deeplink + +import android.content.Intent +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModel + +// TODO: Add tests +/** + * Provides functionality to handle deep links. + * + * Allows deep links to be launched, registered, or unregistered. + */ +interface DeepLinksRegistry { + + /** + * Finds matches registered deep links for the given [intent] and launches them. + * + * @return `true` if any deep link was received, `false` otherwise. + */ + fun launch(intent: Intent): Boolean + + /** + * Registers the given [deepLink]. + * + * @see registerWithLifecycle + * @see registerWithViewModel + */ + fun register(deepLink: DeepLink) + + /** + * Registers the given [deepLinks]. + * + * @see registerWithLifecycle + * @see registerWithViewModel + */ + fun register(deepLinks: Collection) + + /** + * Unregisters the given [deepLinks]. + */ + fun unregister(deepLinks: Collection) + + /** + * Unregisters the given [deepLink]. + */ + fun unregister(deepLink: DeepLink) + + /** + * Unregisters deep links with the given [ids]. + * */ + fun unregisterByIds(ids: Collection) + + /** + * Registers the [deepLinks] when the [owner] is resumed and ensures that they are unregistered when the [owner] is + * stopped. + */ + fun registerWithLifecycle(owner: LifecycleOwner, deepLinks: Collection) + + /** + * Registers the [deepLinks] and ensures that they are unregistered when the [ViewModel] is closed. + */ + fun registerWithViewModel(viewModel: ViewModel, deepLinks: Collection) +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/di/DeepLinksModule.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/di/DeepLinksModule.kt new file mode 100644 index 0000000000..4398db12a2 --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/di/DeepLinksModule.kt @@ -0,0 +1,20 @@ +package com.tangem.core.deeplink.di + +import com.tangem.core.deeplink.DeepLinksRegistry +import com.tangem.core.deeplink.impl.DefaultDeepLinksRegistry +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object DeepLinksModule { + + @Provides + @Singleton + fun provideDeepLinksRegistry(): DeepLinksRegistry { + return DefaultDeepLinksRegistry() + } +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt new file mode 100644 index 0000000000..bf84d3e382 --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt @@ -0,0 +1,173 @@ +package com.tangem.core.deeplink.impl + +import android.content.Intent +import android.net.Uri +import androidx.core.net.toUri +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModel +import com.tangem.core.deeplink.DeepLink +import com.tangem.core.deeplink.DeepLinksRegistry +import com.tangem.core.deeplink.utils.DeepLinksLifecycleObserver +import timber.log.Timber + +internal class DefaultDeepLinksRegistry : DeepLinksRegistry { + + private var registries: List = emptyList() + + override fun launch(intent: Intent): Boolean { + val received = intent.data ?: return false + var hasMatch = false + + Timber.d( + """ + Received deep link intent + |- Received URI: $received + |- Registries: $registries + """.trimIndent(), + ) + registries.forEach { deepLink -> + val expected = deepLink.uri.toUri() + + if (!isMatches(expected, received)) return@forEach + hasMatch = true + + val params = getParams(expected, received) + + Timber.d( + """ + Matched deep link + |- Expected URI: $expected + |- Received URI: $received + |- Params: $params + """.trimIndent(), + ) + deepLink.onReceive(params) + } + + if (!hasMatch) { + Timber.d( + """ + No match found for deep link + |- Received URI: $received + |- Registries: $registries + """.trimIndent(), + ) + } + + return hasMatch + } + + override fun register(deepLinks: Collection) { + registries = (registries + deepLinks).distinctBy(DeepLink::id) + + Timber.d( + """ + Registered deep links + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun register(deepLink: DeepLink) { + registries = (registries + deepLink).distinctBy(DeepLink::id) + + Timber.d( + """ + Registered deep link + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun unregister(deepLinks: Collection) { + registries = registries.filter { it !in deepLinks } + + Timber.d( + """ + Unregistered deep links + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun unregister(deepLink: DeepLink) { + registries = registries.filter { it.id != deepLink.id } + + Timber.d( + """ + Unregistered deep link + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun unregisterByIds(ids: Collection) { + registries = registries.filter { it.id !in ids } + + Timber.d( + """ + Unregistered deep links + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun registerWithLifecycle(owner: LifecycleOwner, deepLinks: Collection) { + val observer = DeepLinksLifecycleObserver(deepLinksRegistry = this, deepLinks) + owner.lifecycle.addObserver(observer) + } + + override fun registerWithViewModel(viewModel: ViewModel, deepLinks: Collection) { + viewModel.addCloseable { + unregister(deepLinks) + } + + register(deepLinks) + } + + private fun isMatches(received: Uri, expected: Uri): Boolean { + if (received == expected) return true + if (received.authority != expected.authority || + received.pathSegments.size != expected.pathSegments.size + ) { + return false + } + + received.pathSegments.forEachIndexed { index, receivedSegment -> + val expectedSegment = expected.pathSegments[index] + if (receivedSegment != expectedSegment && + !(receivedSegment.startsWith(prefix = "{") && receivedSegment.endsWith(suffix = "}")) + ) { + return false + } + } + + return true + } + + private fun getParams(received: Uri, expected: Uri): Map { + val params = mutableMapOf() + + received.pathSegments.forEachIndexed { index, receivedSegment -> + val expectedSegment = expected.pathSegments[index] + if (receivedSegment != expectedSegment && + receivedSegment.startsWith(prefix = "{") && + receivedSegment.endsWith(suffix = "}") + ) { + val path = receivedSegment + .replace(oldValue = "{", newValue = "") + .replace(oldValue = "}", newValue = "") + + params[path] = expectedSegment + } + } + + expected.queryParameterNames.forEach { paramName -> + expected.getQueryParameter(paramName)?.let { param -> + params[paramName] = param + } + } + + return params + } +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/utils/DeepLinksLifecycleObserver.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/utils/DeepLinksLifecycleObserver.kt new file mode 100644 index 0000000000..ae1580e17e --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/utils/DeepLinksLifecycleObserver.kt @@ -0,0 +1,20 @@ +package com.tangem.core.deeplink.utils + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.tangem.core.deeplink.DeepLink +import com.tangem.core.deeplink.DeepLinksRegistry + +internal class DeepLinksLifecycleObserver( + private val deepLinksRegistry: DeepLinksRegistry, + private val deepLinks: Collection, +) : DefaultLifecycleObserver { + + override fun onResume(owner: LifecycleOwner) { + deepLinksRegistry.register(deepLinks) + } + + override fun onPause(owner: LifecycleOwner) { + deepLinksRegistry.unregister(deepLinks) + } +} \ No newline at end of file diff --git a/features/send/api/build.gradle.kts b/features/send/api/build.gradle.kts index 6b742b5542..eb561fcf20 100644 --- a/features/send/api/build.gradle.kts +++ b/features/send/api/build.gradle.kts @@ -10,7 +10,6 @@ android { } dependencies { - implementation(projects.domain.tokens.models) /** AndroidX */ implementation(deps.androidx.fragment.ktx) diff --git a/settings.gradle.kts b/settings.gradle.kts index edf4844e3e..7060d410f2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -70,6 +70,7 @@ include(":core:navigation") include(":core:res") include(":core:ui") include(":core:utils") +include(":core:deep-links") // endregion Core modules // region Libs modules @@ -148,4 +149,4 @@ include(":data:txhistory") include(":data:wallets") include(":data:analytics") include(":data:transaction") -// endregion Data modules +// endregion Data modules \ No newline at end of file From b14caba3cf6aa40d436f37cfc8de508258492d25 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Jan 2024 15:39:56 +0300 Subject: [PATCH 09/15] Updated on 2026-08-14 --- .../ui/components/fields/AmountTextField.kt | 154 ++++++++++++++++++ .../ui/components/fields/SimpleTextField.kt | 99 ++++++++--- .../AmountVisualTransformation.kt | 53 ++++-- .../inputrow/InputRowEnterAmount.kt | 104 ++++++++++++ .../inputrow/InputRowEnterInfoAmount.kt | 94 +++++++++++ .../core/ui/utils/DecimalFormatterExt.kt | 153 +++++++++++++++++ .../core/ui/utils/InputNumberFormatter.kt | 1 + .../com/tangem/domain/tokens/model/Amount.kt | 30 ++++ .../amount/SendAmountCurrencyConverter.kt | 32 ++++ 9 files changed, 677 insertions(+), 43 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Amount.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt new file mode 100644 index 0000000000..d5263bfded --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -0,0 +1,154 @@ +package com.tangem.core.ui.components.fields + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.TopCenter +import androidx.compose.ui.Alignment.Companion.TopStart +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.* +import java.text.DecimalFormat + +@Composable +fun AmountTextField( + value: String, + decimals: Int, + onValueChange: (String) -> Unit, + textStyle: TextStyle, + modifier: Modifier = Modifier, + symbol: String? = null, + color: Color = TangemTheme.colors.text.primary1, + placeholderAlignment: Alignment = TopStart, + showPlaceholder: Boolean = true, + keyboardOptions: KeyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), +) { + val decimalFormat = rememberDecimalFormat() + + val placeholderTextAlign = if (placeholderAlignment == TopCenter) { + TextAlign.Center + } else { + TextAlign.Start + } + SimpleTextField( + value = value, + onValueChange = { newText -> + if (decimalFormat.isValidSymbols(newText)) { + val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals) + onValueChange(trimmed) + } + }, + modifier = modifier + .background(TangemTheme.colors.background.action), + textStyle = textStyle, + color = color, + keyboardOptions = keyboardOptions, + singleLine = true, + visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat), + decorationBox = { innerTextField -> + Box { + if (value.isBlank() && showPlaceholder) { + val placeholder = if (symbol != null) { + decimalFormat.defaultFormat().plus(" $symbol") + } else { + decimalFormat.defaultFormat() + } + Text( + text = placeholder, + style = textStyle, + color = TangemTheme.colors.text.disabled, + textAlign = placeholderTextAlign, + modifier = Modifier + .align(placeholderAlignment), + ) + } + innerTextField() + } + }, + ) +} + +private fun DecimalFormat.isValidSymbols(text: String): Boolean { + return checkDecimalSeparatorDuplicate(text) && checkGroupingSeparator(text) +} + +// region preview +@Preview(locale = "en", showBackground = true, name = "English") +@Preview(locale = "ru", showBackground = true, name = "Russian") +@Composable +private fun AmountTextFieldPreview( + @PreviewParameter(AmountTextFieldPreviewProvider::class) amount: AmountTextFieldPreviewData, +) { + var text by remember { mutableStateOf(amount.value.orEmpty()) } + TangemTheme { + AmountTextField( + value = text, + decimals = amount.decimals, + symbol = amount.symbol, + placeholderAlignment = amount.placeholderAlignment, + showPlaceholder = amount.showPlaceholder, + onValueChange = { text = it }, + textStyle = TangemTheme.typography.h2.copy( + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ), + modifier = Modifier.fillMaxWidth(), + ) + } +} + +private class AmountTextFieldPreviewProvider : PreviewParameterProvider { + override val values = sequenceOf( + AmountTextFieldPreviewData( + symbol = "USD", + value = "1000000,123123", + decimals = 3, + placeholderAlignment = TopStart, + showPlaceholder = true, + ), + AmountTextFieldPreviewData( + symbol = null, + value = "1000000.123123", + decimals = 6, + placeholderAlignment = TopStart, + showPlaceholder = false, + ), + AmountTextFieldPreviewData( + symbol = "$", + value = null, + decimals = 2, + showPlaceholder = true, + placeholderAlignment = TopCenter, + ), + AmountTextFieldPreviewData( + symbol = null, + value = null, + decimals = 2, + showPlaceholder = true, + placeholderAlignment = TopStart, + ), + ) +} + +private data class AmountTextFieldPreviewData( + val symbol: String? = "$", + val value: String? = null, + val decimals: Int = 2, + val showPlaceholder: Boolean, + val placeholderAlignment: Alignment, +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt index 7e0e1643c6..1e29006979 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt @@ -3,14 +3,18 @@ package com.tangem.core.ui.components.fields import androidx.compose.foundation.layout.Box import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference @@ -19,6 +23,7 @@ import com.tangem.core.ui.res.TangemTheme /** * Simple text field with placeholder */ +@Suppress("ReusedModifierInstance") @Composable fun SimpleTextField( value: String, @@ -29,32 +34,72 @@ fun SimpleTextField( visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, color: Color = TangemTheme.colors.text.primary1, + textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color), readOnly: Boolean = false, + decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null, ) { - val focusRequester = remember { FocusRequester() } - BasicTextField( - value = value, - onValueChange = onValueChange, - textStyle = TangemTheme.typography.body2.copy(color = color), - cursorBrush = SolidColor(TangemTheme.colors.text.primary1), - singleLine = singleLine, - readOnly = readOnly, - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, - decorationBox = { textValue -> - Box { - if (value.isBlank() && placeholder != null) { - Text( - text = placeholder.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.disabled, - modifier = Modifier, - ) - } - textValue() - } - }, - modifier = modifier - .focusRequester(focusRequester), + var textFieldValueState by remember { + mutableStateOf( + TextFieldValue( + text = value, + selection = when { + value.isEmpty() -> TextRange.Zero + else -> TextRange(value.length, value.length) + }, + ), + ) + } + val focusRequester = remember { FocusRequester.Default } + val customTextSelectionColors = TextSelectionColors( + handleColor = TangemTheme.colors.text.secondary, + backgroundColor = TangemTheme.colors.text.secondary.copy(alpha = 0.4f), ) + + val textFieldValue = textFieldValueState.copy(text = value) + + SideEffect { + if (textFieldValue.selection != textFieldValueState.selection || + textFieldValue.composition != textFieldValueState.composition + ) { + textFieldValueState = textFieldValue + } + } + + var lastTextValue by remember(value) { mutableStateOf(value) } + + CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) { + BasicTextField( + value = textFieldValue, + onValueChange = { newTextFieldValueState -> + textFieldValueState = newTextFieldValueState + + val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text + lastTextValue = newTextFieldValueState.text + + if (stringChangedSinceLastInvocation) { + onValueChange(newTextFieldValueState.text) + } + }, + textStyle = textStyle.copy(color = color), + cursorBrush = SolidColor(TangemTheme.colors.text.primary1), + singleLine = singleLine, + readOnly = readOnly, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + decorationBox = decorationBox ?: { textValue -> + Box { + if (value.isBlank() && placeholder != null) { + Text( + text = placeholder.resolveReference(), + style = textStyle, + color = TangemTheme.colors.text.disabled, + ) + } + textValue() + } + }, + modifier = modifier + .focusRequester(focusRequester), + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt index b3f27e218e..278c631728 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt @@ -5,28 +5,49 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.OffsetMapping import androidx.compose.ui.text.input.TransformedText import androidx.compose.ui.text.input.VisualTransformation +import com.tangem.core.ui.utils.formatWithThousands +import java.text.DecimalFormat class AmountVisualTransformation( - private val symbol: String, + private val decimals: Int, + private val symbol: String? = null, + private val decimalFormat: DecimalFormat = DecimalFormat(), ) : VisualTransformation { - override fun filter(text: AnnotatedString): TransformedText { - return TransformedText( - buildAnnotatedString { - append(text) - if (text.isNotBlank()) { - append(" ") - append(symbol) - } - }, - object : OffsetMapping { - override fun originalToTransformed(offset: Int): Int { - return text.length - } - override fun transformedToOriginal(offset: Int): Int { - return text.length + override fun filter(text: AnnotatedString): TransformedText { + val formattedText = decimalFormat.formatWithThousands( + text.text, + decimals, + ) + val groupingSymbol = decimalFormat.decimalFormatSymbols.groupingSeparator + return TransformedText( + text = buildAnnotatedString { + append(formattedText) + if (formattedText.isNotEmpty() && symbol != null) { + append(" $symbol") } }, + offsetMapping = OffsetMappingImpl(text.text, formattedText, groupingSymbol), ) } + + private class OffsetMappingImpl( + private val text: String, + private val formattedText: String, + private val gropingSymbol: Char, + ) : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + var noneDigitCount = 0 + var i = 0 + while (i < offset + noneDigitCount) { + if (formattedText.getOrNull(i++) == gropingSymbol) noneDigitCount++ + } + return (offset + noneDigitCount).coerceIn(0, formattedText.length) + } + + override fun transformedToOriginal(offset: Int): Int { + val noneDigitCount = formattedText.take(offset).count { it == gropingSymbol } + return (offset - noneDigitCount).coerceIn(0, text.length) + } + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt new file mode 100644 index 0000000000..d25b9e7bc4 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt @@ -0,0 +1,104 @@ +package com.tangem.core.ui.components.inputrow + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.components.fields.AmountTextField +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Input row for entering amount. Manages correct amount format and validation + * + * @param title title reference + * @param text primary text reference + * @param onValueChange text change callback + * @param modifier modifier + * @param titleColor title color + * @param textColor text color + * @param keyboardOptions keyboard options for field + * @param iconRes action icon + * @param iconTint action icon tint + * @param onIconClick click on action icon + * @param showDivider show divider + * + * @see [InputRowDefault] for read only version + * @see InputRowEnter + */ +@Composable +fun InputRowEnterAmount( + title: TextReference, + text: String, + decimals: Int, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + symbol: String? = null, + titleColor: Color = TangemTheme.colors.text.secondary, + textColor: Color = TangemTheme.colors.text.primary1, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + iconRes: Int? = null, + iconTint: Color = TangemTheme.colors.icon.informative, + onIconClick: (() -> Unit)? = null, + showDivider: Boolean = false, +) { + DividerContainer( + modifier = modifier, + showDivider = showDivider, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.caption2, + color = titleColor, + ) + AmountTextField( + value = text, + decimals = decimals, + symbol = symbol, + onValueChange = onValueChange, + color = textColor, + textStyle = TangemTheme.typography.body2, + keyboardOptions = keyboardOptions, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8), + ) + } + iconRes?.let { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = iconTint, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing10, + bottom = TangemTheme.dimens.spacing10, + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + ) { onIconClick?.invoke() }, + ) + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt new file mode 100644 index 0000000000..273e0ab9ab --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt @@ -0,0 +1,94 @@ +package com.tangem.core.ui.components.inputrow + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.components.fields.AmountTextField +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * `Input Row Enter Info` for entering amount. Manages correct amount format and validation + + * @param title title reference + * @param text primary text reference + * @param onValueChange text change callback + * @param modifier modifier + * @param titleColor title color + * @param textColor text color + * @param isSingleLine text + * @param visualTransformation applied transformation to text + * @param keyboardOptions keyboard options for field + * @param showDivider show divider + * + * @see [InputRowEnterInfo] + * @see Input Row Enter + * @see Input Row Enter Info + */ +@Composable +fun InputRowEnterInfoAmount( + title: TextReference, + text: String, + decimals: Int, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + symbol: String? = null, + info: TextReference? = null, + titleColor: Color = TangemTheme.colors.text.secondary, + textColor: Color = TangemTheme.colors.text.primary1, + infoColor: Color = TangemTheme.colors.text.tertiary, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + showDivider: Boolean = false, +) { + DividerContainer( + modifier = modifier, + showDivider = showDivider, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.caption2, + color = titleColor, + ) + Row { + AmountTextField( + value = text, + decimals = decimals, + symbol = symbol, + onValueChange = onValueChange, + color = textColor, + textStyle = TangemTheme.typography.body2, + keyboardOptions = keyboardOptions, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8) + .weight(1f), + ) + info?.let { + Text( + text = it.resolveReference(), + style = TangemTheme.typography.body2, + color = infoColor, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8) + .align(Alignment.Bottom), + ) + } + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt new file mode 100644 index 0000000000..8cc2cf7bda --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt @@ -0,0 +1,153 @@ +package com.tangem.core.ui.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalConfiguration +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.util.Locale + +private const val TEXT_CHUNK_THOUSAND = 3 +private const val POINT_SEPARATOR = '.' + +@Composable +fun rememberDecimalFormat(): DecimalFormat { + val locale = LocalConfiguration.current.locale + val decimalSymbols = remember { DecimalFormatSymbols.getInstance(locale) } + + return remember { + DecimalFormat().apply { + decimalFormatSymbols = decimalSymbols + isParseBigDecimal = true + } + } +} + +/** + * Formats input [String] for InputField, to remove wrong symbols, letters etc + * Use [decimals] for cut this number symbols after floating point + * + * Example (with 8 decimals): + * input string - ab123.46377372ab53 + * result string 123.46377372 + */ +fun DecimalFormat.getValidatedNumberWithFixedDecimals(text: String, decimals: Int): String { + val thousandsSeparator = decimalFormatSymbols.groupingSeparator + val decimalSeparator = decimalFormatSymbols.decimalSeparator + + val lastChar = text.lastOrNull() + val trimmedText = if (text.isNotEmpty() && (lastChar == thousandsSeparator || lastChar == POINT_SEPARATOR)) { + text.dropLast(1) + decimalSeparator + } else { + text + } + + if (trimmedText.startsWith("0") && trimmedText.length > 1 && trimmedText[1] != decimalSeparator) { + return "0" + } + + val filteredChars = trimmedText.replace(thousandsSeparator.toString(), "").filterIndexed { index, c -> + val isOneOrZeroPoint = + c == decimalSeparator && index != 0 && trimmedText.count { it == decimalSeparator } <= 1 + val isIndexPointIndex = + c == decimalSeparator && index != 0 && trimmedText.indexOf(decimalSeparator) == index + c.isDigit() || isIndexPointIndex || isOneOrZeroPoint + } + // If dot is present, take first digits before decimal and first decimals digits after decimal + return if (filteredChars.count { it == decimalSeparator } == 1) { + val beforeDecimal = filteredChars.substringBefore(decimalSeparator) + val afterDecimal = filteredChars.substringAfter(decimalSeparator) + beforeDecimal + decimalSeparator + afterDecimal.take(decimals) + } + // If there is no dot, just take all digits + else { + filteredChars + } +} + +/** + * Formats input [text] with grouping and decimal separators. + * Takes into account [decimals] number of digits after floating point. + */ +fun DecimalFormat.formatWithThousands(text: String, decimals: Int): String { + val thousandsSeparator = decimalFormatSymbols.groupingSeparator + val decimalSeparator = decimalFormatSymbols.decimalSeparator + val localizedText = text.replace("[,.]".toRegex(), decimalSeparator.toString()) + return if (localizedText.count { it == decimalSeparator } == 1) { + val beforeDecimal = localizedText.substringBefore(decimalSeparator) + .reversed() + .chunked(TEXT_CHUNK_THOUSAND) + .joinToString(thousandsSeparator.toString()) + .reversed() + val afterDecimal = localizedText.substringAfter(decimalSeparator) + beforeDecimal + decimalSeparator + afterDecimal.take(decimals) + } + // If there is no dot, just take all digits + else { + localizedText.reversed() + .chunked(TEXT_CHUNK_THOUSAND) + .joinToString(thousandsSeparator.toString()) + .reversed() + } +} + +fun DecimalFormat.defaultFormat(): String { + return "0${decimalFormatSymbols.decimalSeparator}00" +} + +/** + * Checks if text input contains extra decimal separators. + * If so, it will return false, otherwise true. + * + * Note: number can contain only one decimal separator. + */ +fun DecimalFormat.checkDecimalSeparatorDuplicate(text: String): Boolean { + val regex = "[${decimalFormatSymbols.decimalSeparator}]".toRegex() + val decimalSeparatorCount = regex.findAll(text).count() + return decimalSeparatorCount <= 1 // only one decimal separator +} + +/** + * Checks if text input contains grouping separators. + * If so, it will return false, otherwise true. + * + * Note: grouping separators are used only for VisualTransformations. + */ +fun DecimalFormat.checkGroupingSeparator(text: String): Boolean { + val regex = "[${decimalFormatSymbols.groupingSeparator}]".toRegex() + val decimalSeparatorCount = regex.findAll(text).count() + return decimalSeparatorCount == 0 // no grouping separator +} + +fun String.parseToBigDecimal(decimals: Int): BigDecimal { + val decimalFormat = DecimalFormat().apply { + decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault()) + isParseBigDecimal = true + maximumFractionDigits = decimals + minimumFractionDigits = decimals + } + return try { + decimalFormat.parse(this) as? BigDecimal ?: BigDecimal.ZERO + } catch (e: Exception) { + BigDecimal.ZERO + } +} + +fun BigDecimal.parseBigDecimal(decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN): String { + val decimalFormat = DecimalFormat().apply { + decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault()) + isParseBigDecimal = true + isGroupingUsed = false + maximumFractionDigits = decimals + minimumFractionDigits = 0 + this.roundingMode = roundingMode + } + + return try { + decimalFormat.format(this) + } catch (e: Exception) { + "" + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/InputNumberFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/InputNumberFormatter.kt index 7ed7c4a7c4..7873012994 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/InputNumberFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/InputNumberFormatter.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.utils import java.text.DecimalFormat +@Deprecated("Deprecated due to unnecessary abstraction. Use methods from DecimalFormatterExt") class InputNumberFormatter( numberFormat: DecimalFormat, ) { diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Amount.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Amount.kt new file mode 100644 index 0000000000..eb777dbcca --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Amount.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.tokens.model + +import java.math.BigDecimal + +data class Amount( + val currencySymbol: String, + val value: BigDecimal? = null, + val decimals: Int, + val type: AmountType = AmountType.CoinType, +) + +sealed class AmountType { + object CoinType : AmountType() + object ReserveType : AmountType() + data class TokenType(val token: CryptoCurrency.Token) : AmountType() + data class FiatType(val code: String) : AmountType() +} + +/** Converts `BigDecimal` [cryptoCurrency] to [Amount] */ +fun BigDecimal.convertToAmount(cryptoCurrency: CryptoCurrency) = Amount( + currencySymbol = cryptoCurrency.symbol, + value = this, + decimals = cryptoCurrency.decimals, + type = when (cryptoCurrency) { + is CryptoCurrency.Coin -> AmountType.CoinType + is CryptoCurrency.Token -> AmountType.TokenType( + token = cryptoCurrency, + ) + }, +) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt new file mode 100644 index 0000000000..0f95e9fad7 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt @@ -0,0 +1,32 @@ +package com.tangem.features.send.impl.presentation.state.amount + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero + +internal class SendAmountCurrencyConverter( + private val currentStateProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + override fun convert(value: Boolean): SendUiState { + val state = currentStateProvider() + val amountState = state.amountState ?: return state + val amountTextField = amountState.amountTextField + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + + val isValidFiatRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero() + return if (amountTextField.isFiatValue == value && !isValidFiatRate) { + state + } else { + return state.copy( + amountState = amountState.copy( + amountTextField = amountTextField.copy( + isFiatValue = value, + ), + ), + ) + } + } +} \ No newline at end of file From a2d1834687a9196810160741db9af9dde4eb1fed Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Jan 2024 15:40:17 +0300 Subject: [PATCH 10/15] Updated on 2026-08-14 --- .../ui/components/fields/AmountTextField.kt | 18 +++ .../presentation/state/SendStateFactory.kt | 39 +++--- .../impl/presentation/state/SendUiState.kt | 26 ++-- .../state/amount/SendAmountStateConverter.kt | 10 +- .../presentation/state/fee/FeeCalculation.kt | 6 +- .../presentation/state/fee/FeeStateFactory.kt | 12 +- .../state/fee/SendFeeCustomFieldConverter.kt | 25 +++- .../state/fee/SendFeeStateConverter.kt | 16 ++- .../fields/SendAmountFieldChangeConverter.kt | 121 +++++++++--------- .../state/fields/SendAmountFieldConverter.kt | 34 +++-- .../state/fields/SendTextField.kt | 14 +- .../presentation/ui/amount/AmountField.kt | 96 ++++---------- .../ui/amount/AmountFieldContainer.kt | 17 +-- .../ui/fee/SendCustomFeeEthereum.kt | 104 ++++++--------- .../ui/fee/SendSpeedAndFeeContent.kt | 12 +- .../impl/presentation/ui/send/SendContent.kt | 16 ++- .../presentation/viewmodel/SendViewModel.kt | 18 ++- 17 files changed, 285 insertions(+), 299 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index d5263bfded..59c9b5143d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -22,6 +22,24 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.* import java.text.DecimalFormat +/** + * Simple text field for amount input. + * Validates and trims input text using [DecimalFormat]. Formats visual output using [AmountVisualTransformation]. + * Can display aligned placeholder and currency symbol [symbol]. + * + * @param value initial text + * @param decimals number of decimal places + * @param onValueChange callback + * @param textStyle text and placeholder styles + * @param modifier modifier + * @param symbol currency symbol + * @param color text color + * @param placeholderAlignment alignment of placeholder + * @param showPlaceholder show placeholder + * @param keyboardOptions keyboard options + * + * @see [SimpleTextField] for standard text field + */ @Composable fun AmountTextField( value: String, 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 185a62b42a..23b9ce0dfb 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 @@ -14,6 +14,7 @@ import com.tangem.domain.wallets.models.UserWallet 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.SendAmountCurrencyConverter import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter @@ -39,8 +40,25 @@ 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 amountFieldConverter by lazy { + SendAmountFieldConverter( + clickIntents = clickIntents, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + appCurrencyProvider = appCurrencyProvider, + ) + } + private val amountFieldChangeConverter by lazy { + SendAmountFieldChangeConverter( + currentStateProvider = currentStateProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + private val amountCurrencyConverter by lazy { + SendAmountCurrencyConverter( + currentStateProvider = currentStateProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } private val amountStateConverter by lazy { SendAmountStateConverter( @@ -57,11 +75,7 @@ internal class SendStateFactory( cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } - private val feeStateConverter by lazy { - SendFeeStateConverter( - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, - ) - } + private val feeStateConverter by lazy { SendFeeStateConverter() } private val recipientListStateConverter by lazy { SendRecipientListConverter( @@ -90,16 +104,7 @@ internal class SendStateFactory( //region amount state clicks fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value) - fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState { - val state = currentStateProvider() - val amountState = state.amountState ?: return state - - return if (amountState.isFiatValue == isFiat) { - state - } else { - return state.copy(amountState = amountState.copy(isFiatValue = isFiat)) - } - } + fun getOnCurrencyChangedState(isFiat: Boolean) = amountCurrencyConverter.convert(isFiat) //endregion //region recipient 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 f02b6ddf13..ea8245e94c 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 @@ -6,8 +6,7 @@ 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.core.ui.extensions.TextReference import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState @@ -46,15 +45,11 @@ internal sealed class SendStates { data class AmountState( override val type: SendUiStateType = SendUiStateType.Amount, override val isPrimaryButtonEnabled: Boolean, - val cryptoCurrencyStatus: CryptoCurrencyStatus, - val appCurrency: AppCurrency, val walletName: String, - val walletBalance: String, + val walletBalance: TextReference, val tokenIconState: TokenIconState, - val isFiatValue: Boolean, val segmentedButtonConfig: PersistentList, - val amountTextField: SendTextField.Amount, - val amountValue: BigDecimal, + val amountTextField: SendTextField.AmountField, ) : SendStates() /** Recipient state */ @@ -72,14 +67,13 @@ internal sealed class SendStates { data class FeeState( override val type: SendUiStateType = SendUiStateType.Fee, override val isPrimaryButtonEnabled: Boolean = false, - 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 = "", - val notifications: ImmutableList = persistentListOf(), + val feeSelectorState: FeeSelectorState, + val isSubtract: Boolean, + val isUserSubtracted: Boolean, + val fee: Fee?, + val receivedAmountValue: BigDecimal, + val receivedAmount: String, + val notifications: ImmutableList, ) : SendStates() /** Send state */ 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 3420a483f4..5bda8cc991 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 @@ -1,18 +1,20 @@ package com.tangem.features.send.impl.presentation.state.amount import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount 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.R import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter 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, @@ -30,15 +32,11 @@ internal class SendAmountStateConverter( val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals) return SendStates.AmountState( - appCurrency = appCurrency, - cryptoCurrencyStatus = status, walletName = userWallet.name, - walletBalance = "$crypto ($fiat)", + walletBalance = resourceReference(R.string.send_wallet_balance_format, wrappedList(crypto, fiat)), tokenIconState = iconStateConverter.convert(status), 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 de65f8a987..cd0877210f 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 @@ -2,6 +2,7 @@ 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.blockchain.extensions.toBigDecimalOrDefault import com.tangem.features.send.impl.presentation.state.SendUiState import java.math.BigDecimal @@ -9,7 +10,7 @@ import java.math.BigDecimal * Calculate receiving amount when fee is subtracted from sending amount */ internal fun calculateReceiveAmount(state: SendUiState, feeAmount: Fee): BigDecimal { - val amountValue = state.amountState?.amountValue ?: BigDecimal.ZERO + val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO val fee = feeAmount.amount.value ?: return BigDecimal.ZERO return amountValue.minus(fee) } @@ -25,8 +26,7 @@ internal fun FeeSelectorState.Content.getFee(): Fee { FeeType.MARKET -> fees.normal FeeType.FAST -> fees.priority FeeType.CUSTOM -> { - val feeAmount = - customValues.firstOrNull()?.value?.let { BigDecimal(it.ifEmpty { "0" }) } ?: BigDecimal.ZERO + val feeAmount = customValues.firstOrNull()?.value.toBigDecimalOrDefault() Fee.Common( fees.normal.amount.copy( value = feeAmount, 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 index 9ad8b30568..8a1073cad3 100644 --- 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 @@ -76,7 +76,7 @@ internal class FeeStateFactory( feeState = updatedState.copy( notifications = feeNotificationFactory( feeState = updatedState, - amountValue = state.amountState?.amountValue, + amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, ), isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), ), @@ -92,7 +92,7 @@ internal class FeeStateFactory( feeState = updatedState?.copy( notifications = feeNotificationFactory( feeState = updatedState, - amountValue = state.amountState?.amountValue, + amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, ), ), ) @@ -119,7 +119,7 @@ internal class FeeStateFactory( feeState = updatedState.copy( notifications = feeNotificationFactory( feeState = updatedState, - amountValue = state.amountState?.amountValue, + amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, ), isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), ), @@ -150,7 +150,7 @@ internal class FeeStateFactory( feeState = updatedState.copy( notifications = feeNotificationFactory( feeState = updatedState, - amountValue = state.amountState?.amountValue, + amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, ), isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), ), @@ -174,7 +174,7 @@ internal class FeeStateFactory( feeState = updatedState.copy( notifications = feeNotificationFactory( feeState = updatedState, - amountValue = state.amountState?.amountValue, + amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, ), isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), ), @@ -203,7 +203,7 @@ internal class FeeStateFactory( private fun checkAutoSubtract(state: SendUiState, fee: Fee, balance: BigDecimal): Boolean { val feeState = state.feeState ?: return false - val amountValue = state.amountState?.amountValue ?: BigDecimal.ZERO + val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO val feeAmount = fee.amount.value ?: BigDecimal.ZERO return if (feeState.isUserSubtracted) { feeState.isSubtract diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt index ff9da4a71f..d6d6168f37 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt @@ -4,13 +4,16 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.blockchain.common.transaction.Fee -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import com.tangem.utils.toFormattedString import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -31,16 +34,24 @@ internal class SendFeeCustomFieldConverter( return persistentListOf( SendTextField.CustomFee( - value = ethereumFee.amount.value.toString(), + value = ethereumFee.amount.value?.toFormattedString(ethereumFee.amount.decimals).orEmpty(), + decimals = ethereumFee.amount.decimals, + symbol = ethereumFee.amount.currencySymbol, onValueChange = { clickIntents.onCustomFeeValueChange(0, it) }, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next, keyboardType = KeyboardType.Number, ), - label = TextReference.Str(maxFeeFiat), + title = resourceReference(R.string.send_max_fee), + footer = resourceReference(R.string.send_max_fee_footer), + label = stringReference(maxFeeFiat), ), SendTextField.CustomFee( value = ethereumFee.gasPrice.toString(), + decimals = 0, + symbol = ETHEREUM_UNIT, + title = resourceReference(R.string.send_gas_price), + footer = resourceReference(R.string.send_gas_price_footer), onValueChange = { clickIntents.onCustomFeeValueChange(1, it) }, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next, @@ -49,6 +60,10 @@ internal class SendFeeCustomFieldConverter( ), SendTextField.CustomFee( value = ethereumFee.gasLimit.toString(), + decimals = 0, + symbol = null, + title = resourceReference(R.string.send_gas_limit), + footer = resourceReference(R.string.send_gas_limit_footer), onValueChange = { clickIntents.onCustomFeeValueChange(2, it) }, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Done, @@ -57,4 +72,8 @@ internal class SendFeeCustomFieldConverter( ), ) } + + companion object { + private const val ETHEREUM_UNIT = "GWEI" + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt index d87f1a2795..ccd5f4ae89 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -1,17 +1,21 @@ package com.tangem.features.send.impl.presentation.state.fee -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendStates -import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal -internal class SendFeeStateConverter( - private val cryptoCurrencyStatusProvider: Provider, -) : Converter { +internal class SendFeeStateConverter : Converter { override fun convert(value: Unit): SendStates.FeeState { return SendStates.FeeState( - cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + feeSelectorState = FeeSelectorState.Loading, + isSubtract = false, + isUserSubtracted = false, + fee = null, + receivedAmountValue = BigDecimal.ZERO, + receivedAmount = "", + notifications = persistentListOf(), ) } } \ No newline at end of file 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 b21b9540ac..7a6c696c1a 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,59 +1,49 @@ package com.tangem.features.send.impl.presentation.state.fields -import com.tangem.blockchain.extensions.toBigDecimalOrDefault +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import java.math.BigDecimal -import java.text.NumberFormat +import java.math.RoundingMode internal class SendAmountFieldChangeConverter( private val currentStateProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, ) : Converter { override fun convert(value: String): SendUiState { val state = currentStateProvider() val amountState = state.amountState ?: return state + val amountTextField = amountState.amountTextField val feeState = state.feeState ?: return state - if (value.checkDecimalSeparatorDuplicate()) return state if (value.isEmpty()) return state.emptyState() - val fiatRate = amountState.cryptoCurrencyStatus.value.fiatRate + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + val trimmedValue = value.trim() - val cryptoValue = if (amountState.isFiatValue) { - if (value.isNotBlank()) { - trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty() - } else { - DEFAULT_VALUE - } - } else { - trimmedValue - } + val cryptoValue = trimmedValue.getCryptoValue(amountTextField.isFiatValue, cryptoDecimals) + val fiatValue = trimmedValue.getFiatValue(amountTextField.isFiatValue, fiatDecimals) + val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals) + val decimalFiatValue = fiatValue.parseToBigDecimal(fiatDecimals) - val fiatValue = if (!amountState.isFiatValue) { - if (value.isNotBlank()) { - trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty() - } else { - DEFAULT_VALUE - } - } else { - trimmedValue - } - - val checkValue = if (amountState.isFiatValue) fiatValue else cryptoValue - val isExceedBalance = checkValue.checkExceedBalance(amountState) - val isMaxAmount = checkValue.checkMaxAmount(amountState) + val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue + val isExceedBalance = checkValue.checkExceedBalance(amountTextField) + val isMaxAmount = checkValue.checkMaxAmount(amountTextField) return state.copy( amountState = amountState.copy( isPrimaryButtonEnabled = !isExceedBalance, - amountTextField = amountState.amountTextField.copy( + amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, isError = isExceedBalance, + cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), ), - amountValue = cryptoValue.toBigDecimalOrDefault(), ), feeState = feeState.copy( isSubtract = isMaxAmount, @@ -61,58 +51,63 @@ internal class SendAmountFieldChangeConverter( ) } + private fun String.getCryptoValue(isFiatValue: Boolean, decimals: Int): String { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val fiatRate = cryptoCurrencyStatus.value.fiatRate + return if (isFiatValue && fiatRate != null) { + parseToBigDecimal(decimals).divide(fiatRate, decimals, RoundingMode.DOWN) + .parseBigDecimal(decimals) + } else { + this + } + } + + private fun String.getFiatValue(isFiatValue: Boolean, decimals: Int): String { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val fiatRate = cryptoCurrencyStatus.value.fiatRate + return if (!isFiatValue && fiatRate != null) { + parseToBigDecimal(decimals).multiply(fiatRate).parseBigDecimal(decimals) + } else { + this + } + } + private fun SendUiState.emptyState(): SendUiState { return copy( amountState = amountState?.copy( isPrimaryButtonEnabled = false, amountTextField = amountState.amountTextField.copy( - value = if (!amountState.isFiatValue) "" else DEFAULT_VALUE, - fiatValue = if (amountState.isFiatValue) "" else DEFAULT_VALUE, + value = "", + fiatValue = "", isError = false, ), ), ) } - private fun String.checkDecimalSeparatorDuplicate(): Boolean { - val regex = TRIM_REGEX.toRegex() - val decimalSeparatorCount = regex.findAll(this).count() - - return decimalSeparatorCount > 1 - } - - private fun String.checkExceedBalance(state: SendStates.AmountState): Boolean { - val currencyCryptoAmount = state.cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO - val currencyFiatAmount = state.cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO - return if (state.isFiatValue) { - toBigDecimal() > currencyFiatAmount + private fun String.checkExceedBalance(amountTextField: SendTextField.AmountField): Boolean { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO + return if (amountTextField.isFiatValue) { + parseToBigDecimal(amountTextField.fiatAmount.decimals) > currencyFiatAmount } else { - toBigDecimal() > currencyCryptoAmount + parseToBigDecimal(amountTextField.cryptoAmount.decimals) > currencyCryptoAmount } } - private fun String.checkMaxAmount(state: SendStates.AmountState): Boolean { + private fun String.checkMaxAmount(amountTextField: SendTextField.AmountField): Boolean { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + // If current currency is Token - if (state.cryptoCurrencyStatus.currency is CryptoCurrency.Token) return false + if (cryptoCurrencyStatus.currency is CryptoCurrency.Token) return false - val currencyCryptoAmount = state.cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO - val currencyFiatAmount = state.cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO - return if (state.isFiatValue) { - toBigDecimal() == currencyFiatAmount + val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO + return if (amountTextField.isFiatValue) { + parseToBigDecimal(amountTextField.fiatAmount.decimals) == currencyFiatAmount } else { - toBigDecimal() == currencyCryptoAmount + parseToBigDecimal(amountTextField.cryptoAmount.decimals) == currencyCryptoAmount } } - - private fun String.trim(): String { - var trimmedValue = this - if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1) - - return trimmedValue.replace(TRIM_REGEX.toRegex(), ".") - } - - companion object { - private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00) - private const val TRIM_REGEX = "[.,]" - } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt index 9ab26bd83d..25a63f78f8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt @@ -4,31 +4,47 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.Amount +import com.tangem.domain.tokens.model.AmountType +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.convertToAmount import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import java.text.NumberFormat +import java.math.BigDecimal + +private const val FIAT_DECIMALS = 2 internal class SendAmountFieldConverter( private val clickIntents: SendClickIntents, -) : Converter { + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, +) : Converter { - override fun convert(value: Unit): SendTextField.Amount { - return SendTextField.Amount( + override fun convert(value: Unit): SendTextField.AmountField { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + return SendTextField.AmountField( value = "", - fiatValue = DEFAULT_VALUE, + fiatValue = "", onValueChange = clickIntents::onAmountValueChange, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next, keyboardType = KeyboardType.Number, ), - placeholder = TextReference.Str(DEFAULT_VALUE), + isFiatValue = false, + cryptoAmount = BigDecimal.ZERO.convertToAmount(cryptoCurrencyStatus.currency), + fiatAmount = getAppCurrencyAmount(appCurrencyProvider()), isError = false, error = TextReference.Res(R.string.swapping_insufficient_funds), ) } - companion object { - private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00) - } + private fun getAppCurrencyAmount(appCurrency: AppCurrency) = Amount( + currencySymbol = appCurrency.symbol, + value = BigDecimal.ZERO, + decimals = FIAT_DECIMALS, + type = AmountType.FiatType(appCurrency.code), + ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt index 456359a16c..42b2ac5b0d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt @@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.state.fields import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.tokens.model.Amount @Immutable internal sealed class SendTextField { @@ -16,14 +17,13 @@ internal sealed class SendTextField { /** Keyboard options */ abstract val keyboardOptions: KeyboardOptions - // /** Placeholder (hint) */ - // abstract val placeholder: TextReference - - data class Amount( + data class AmountField( override val value: String, override val onValueChange: (String) -> Unit, override val keyboardOptions: KeyboardOptions, - val placeholder: TextReference, + val cryptoAmount: Amount, + val fiatAmount: Amount, + val isFiatValue: Boolean, val fiatValue: String, val isError: Boolean, val error: TextReference, @@ -53,6 +53,10 @@ internal sealed class SendTextField { override val value: String, override val onValueChange: (String) -> Unit, override val keyboardOptions: KeyboardOptions, + val symbol: String?, + val decimals: Int, + val title: TextReference, + val footer: TextReference, val label: TextReference? = null, ) : SendTextField() } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt index f6471c020d..c2ca7a26d8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt @@ -3,48 +3,49 @@ package com.tangem.features.send.impl.presentation.ui.amount import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.BottomCenter -import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.text.style.TextAlign -import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation +import com.tangem.core.ui.components.fields.AmountTextField 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.defaultFormat +import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.features.send.impl.presentation.state.fields.SendTextField @Composable -internal fun ColumnScope.AmountField( - sendField: SendTextField.Amount, - cryptoSymbol: String, - fiatSymbol: String, - isFiat: Boolean, -) { - val value = if (isFiat) sendField.fiatValue else sendField.value - val secondaryValue = if (!isFiat) sendField.fiatValue else sendField.value - val symbol = if (isFiat) fiatSymbol else cryptoSymbol - val secondarySymbol = if (!isFiat) fiatSymbol else cryptoSymbol +internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean) { + val decimalFormat = rememberDecimalFormat() + val (primaryValue, secondaryValue) = if (isFiat) { + sendField.fiatValue to sendField.value + } else { + sendField.value to sendField.fiatValue + } - AmountFieldInner( - value = value, - placeholder = sendField.placeholder, - symbol = symbol, + val (primaryAmount, secondaryAmount) = if (!isFiat) { + sendField.cryptoAmount to sendField.fiatAmount + } else { + sendField.fiatAmount to sendField.cryptoAmount + } + + AmountTextField( + value = primaryValue, + decimals = primaryAmount.decimals, + symbol = primaryAmount.currencySymbol, onValueChange = sendField.onValueChange, keyboardOptions = sendField.keyboardOptions, + textStyle = TangemTheme.typography.h2.copy( + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ), + placeholderAlignment = TopCenter, modifier = Modifier - .align(CenterHorizontally) .padding( top = TangemTheme.dimens.spacing24, start = TangemTheme.dimens.spacing12, @@ -54,15 +55,15 @@ internal fun ColumnScope.AmountField( Box( modifier = Modifier - .align(CenterHorizontally) .padding( top = TangemTheme.dimens.spacing8, start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ), ) { + val text = "${secondaryValue.ifEmpty { decimalFormat.defaultFormat() }} ${secondaryAmount.currencySymbol}" Text( - text = "$secondaryValue $secondarySymbol", + text = text, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, @@ -80,47 +81,6 @@ internal fun ColumnScope.AmountField( } } -@Composable -private fun AmountFieldInner( - value: String, - placeholder: TextReference, - symbol: String, - onValueChange: (String) -> Unit, - keyboardOptions: KeyboardOptions, - modifier: Modifier = Modifier, -) { - val focusRequester = remember { FocusRequester() } - BasicTextField( - value = value, - onValueChange = onValueChange, - modifier = modifier - .focusRequester(focusRequester) - .background(TangemTheme.colors.background.action), - textStyle = TangemTheme.typography.h2.copy( - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ), - keyboardOptions = keyboardOptions, - singleLine = true, - visualTransformation = AmountVisualTransformation(symbol), - decorationBox = { innerTextField -> - Box { - if (value.isBlank()) { - Text( - text = "${placeholder.resolveReference()} $symbol", - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.disabled, - textAlign = TextAlign.Center, - modifier = Modifier - .align(Alignment.TopCenter), - ) - } - innerTextField() - } - }, - ) -} - @Composable private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: Modifier = Modifier) { AnimatedVisibility( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt index 4f95280719..afd7b30b6e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt @@ -12,12 +12,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendStates @Composable internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier: Modifier = Modifier) { Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier .fillMaxWidth() .padding( @@ -33,29 +35,24 @@ internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier: style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing14) - .align(Alignment.CenterHorizontally), + .padding(top = TangemTheme.dimens.spacing14), ) Text( - text = amountState.walletBalance, + text = amountState.walletBalance.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing2) - .align(Alignment.CenterHorizontally), + .padding(top = TangemTheme.dimens.spacing2), ) TokenIcon( state = amountState.tokenIconState, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing32) - .align(Alignment.CenterHorizontally), + .padding(top = TangemTheme.dimens.spacing32), ) AmountField( sendField = amountState.amountTextField, - isFiat = amountState.isFiatValue, - cryptoSymbol = amountState.cryptoCurrencyStatus.currency.symbol, - fiatSymbol = amountState.appCurrency.symbol, + isFiat = amountState.amountTextField.isFiatValue, ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt index 69bc85a649..a5eba38a01 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt @@ -5,86 +5,64 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation -import com.tangem.core.ui.components.inputrow.InputRowEnter -import com.tangem.core.ui.components.inputrow.InputRowEnterInfo -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.components.inputrow.InputRowEnterAmount +import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.ui.common.FooterContainer import kotlinx.collections.immutable.ImmutableList -private const val ETHEREUM_UNIT = "GWEI" - @Composable internal fun SendCustomFeeEthereum( customValues: ImmutableList, selectedFee: FeeType, - symbol: String, modifier: Modifier = Modifier, ) { if (selectedFee == FeeType.CUSTOM && customValues.isNotEmpty()) { - val fee = customValues[0] - val gasPrice = customValues[1] - val gasLimit = customValues[2] - Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), modifier = modifier, ) { - FooterContainer( - footer = stringResource(R.string.send_max_fee_footer), - ) { - InputRowEnterInfo( - text = fee.value, - title = TextReference.Res(R.string.send_max_fee), - info = fee.label, - visualTransformation = AmountVisualTransformation(symbol), - keyboardOptions = fee.keyboardOptions, - onValueChange = fee.onValueChange, - isSingleLine = true, - modifier = Modifier - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), - ) - } - FooterContainer( - footer = stringResource(R.string.send_gas_price_footer), - ) { - InputRowEnter( - text = gasPrice.value, - title = TextReference.Res(R.string.send_gas_price), - onValueChange = gasPrice.onValueChange, - visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT), - keyboardOptions = fee.keyboardOptions, - isSingleLine = true, - modifier = Modifier - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), - ) - } - FooterContainer( - footer = stringResource(R.string.send_gas_limit_footer), - ) { - InputRowEnter( - text = gasLimit.value, - title = TextReference.Res(R.string.send_gas_limit), - onValueChange = gasLimit.onValueChange, - keyboardOptions = fee.keyboardOptions, - isSingleLine = true, - modifier = Modifier - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), - ) + repeat(customValues.size) { index -> + val value = customValues[index] + FooterContainer( + footer = value.footer.resolveReference(), + ) { + if (value.label != null) { + InputRowEnterInfoAmount( + text = value.value, + decimals = value.decimals, + symbol = value.symbol, + title = value.title, + info = value.label, + keyboardOptions = value.keyboardOptions, + onValueChange = value.onValueChange, + showDivider = false, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } else { + InputRowEnterAmount( + text = value.value, + decimals = value.decimals, + title = value.title, + symbol = value.symbol, + onValueChange = value.onValueChange, + keyboardOptions = value.keyboardOptions, + showDivider = false, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } + } } } } 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 2bd84ea7ba..c490f4b84b 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 @@ -43,10 +43,7 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S clickIntents = clickIntents, ) } - customFee( - feeSendState = feeSendState, - cryptoCurrencySymbol = state.cryptoCurrencyStatus.currency.symbol, - ) + customFee(feeSendState) notifications(notifications) subtractButton( receivedAmount = state.receivedAmount, @@ -84,11 +81,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList? { val amountState = uiState.amountState ?: return null val recipientState = uiState.recipientState ?: return null - val amount = amountState.amountTextField.value.toBigDecimal() + val amount = amountState.amountTextField.cryptoAmount.value ?: return null return getFeeUseCase.invoke( amount = amount, @@ -495,7 +500,7 @@ 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 amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return val amountToSend = if (feeState.isSubtract) feeState.receivedAmountValue else amountValue @@ -616,7 +621,6 @@ internal class SendViewModel @Inject constructor( companion object { private const val XRP_X_ADDRESS = 'X' - private const val DEFAULT_VALUE = "0.00" private const val CHECK_FEE_UPDATE_DELAY = 60_000L private const val BALANCE_UPDATE_DELAY = 10_000L } From 885db20951752b25385c307f28e238d883485233 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Jan 2024 12:49:58 +0300 Subject: [PATCH 11/15] Updated on 2026-08-14 --- .../main/res/values-de/strings-blockchain.xml | 1 + .../main/res/values-fr/strings-blockchain.xml | 1 + .../main/res/values-it/strings-blockchain.xml | 1 + .../main/res/values-ru/strings-blockchain.xml | 4 ++-- .../res/values-zh-rTW/strings-blockchain.xml | 1 + .../main/res/values/strings-blockchain.xml | 3 ++- .../core/ui/extensions/BlockchainIcons.kt | 5 ++++ core/ui/src/main/res/drawable/ic_aptos_22.xml | 10 ++++++++ .../ui/src/main/res/drawable/img_aptos_22.xml | 16 +++++++++++++ .../repository/DefaultNetworksRepository.kt | 7 ++++++ .../domain/common/extensions/Blockchain.kt | 2 ++ .../model/warnings/CryptoCurrencyWarning.kt | 2 ++ .../tokens/GetCurrencyWarningsUseCase.kt | 12 ++++++---- .../tokens/repository/NetworksRepository.kt | 2 ++ .../repository/MockNetworksRepository.kt | 2 ++ ...okenDetailsNotificationsAnalyticsSender.kt | 1 + .../components/TokenDetailsNotification.kt | 5 ++++ .../TokenDetailsNotificationConverter.kt | 24 +++++++++---------- gradle/dependencies.toml | 2 +- 19 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_aptos_22.xml create mode 100644 core/ui/src/main/res/drawable/img_aptos_22.xml diff --git a/core/res/src/main/res/values-de/strings-blockchain.xml b/core/res/src/main/res/values-de/strings-blockchain.xml index d438a29cd8..82986f8529 100644 --- a/core/res/src/main/res/values-de/strings-blockchain.xml +++ b/core/res/src/main/res/values-de/strings-blockchain.xml @@ -1,6 +1,7 @@ Erhalt der Gebühr fehlgeschlagen + Senden Sie Geld an diese Andresse um ein Konto zu erstellen Minimaler Betrag ist %s Restbestand zu klein Falsche Gebühr diff --git a/core/res/src/main/res/values-fr/strings-blockchain.xml b/core/res/src/main/res/values-fr/strings-blockchain.xml index 0f4d6dccec..97b9e7c7c7 100644 --- a/core/res/src/main/res/values-fr/strings-blockchain.xml +++ b/core/res/src/main/res/values-fr/strings-blockchain.xml @@ -1,6 +1,7 @@ Échec de réception des commissions + Pour créer un compte, envoyez des fonds monétaires à cette adresse Le montant minimal est de %s Le reste est trop petit Commission non valide diff --git a/core/res/src/main/res/values-it/strings-blockchain.xml b/core/res/src/main/res/values-it/strings-blockchain.xml index 338c03f0da..bedc136ed0 100644 --- a/core/res/src/main/res/values-it/strings-blockchain.xml +++ b/core/res/src/main/res/values-it/strings-blockchain.xml @@ -1,6 +1,7 @@ Impossibile ottenere la commissione + Per creare un account, invia fondi a questo indirizzo L\'importo minimo è di %s L\'importo residuo è molto basso Commissione non valida diff --git a/core/res/src/main/res/values-ru/strings-blockchain.xml b/core/res/src/main/res/values-ru/strings-blockchain.xml index 0744b44f6b..c58315db3f 100644 --- a/core/res/src/main/res/values-ru/strings-blockchain.xml +++ b/core/res/src/main/res/values-ru/strings-blockchain.xml @@ -6,8 +6,8 @@ Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. Недостаточно средств для совершения транзакции. Пожалуйста, пополните свой аккаунт. Произошла ошибка. Код: %s. - Из-за ограничений Kaspa в одну транзакцию может поместиться только %1$d UTXO. Это означает, что вы можете отправить только %2$s или меньше. Вам нужно уменьшить сумму. - Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе. + Для создания аккаунта отправьте средства на этот адрес + Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта. Минимальная сумма: %s Сдача слишком мала diff --git a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml index 607268ec3a..eb43a8cf6f 100644 --- a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml +++ b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml @@ -4,6 +4,7 @@ 遺留資產 獲取費用失敗 由於 Kaspa 的限制,只有%1$d UTXO 可以放入單次交易中。這意味著您只能發送%2$s或更少數量。您需要減少數量。 + 要創建帳戶,請將資金發送到此地址 目標帳戶未激活。發送 %s 或更多以激活帳戶 最小數量是 %s 更動太小 diff --git a/core/res/src/main/res/values/strings-blockchain.xml b/core/res/src/main/res/values/strings-blockchain.xml index bb28ad284c..2aeb56c771 100644 --- a/core/res/src/main/res/values/strings-blockchain.xml +++ b/core/res/src/main/res/values/strings-blockchain.xml @@ -7,7 +7,8 @@ Not enough funds for the transaction. Please top up your account. An error occurred. Code: %s. Due to Kaspa limitations only %1$d UTXOs can fit in a single transaction. This means you can only send %2$s or less. You need to reduce the amount. - To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely. + To create account send funds to this address + To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely Destination account is not active. Send %s or more to activate the account. Minimum amount is %s Change is too small diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 1fbc40be6a..e3170a4467 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -48,6 +48,7 @@ fun getActiveIconRes(blockchainId: String): Int { "decimal", "decimal/test" -> R.drawable.img_decimal_22 "xdc", "xdc/test" -> R.drawable.img_xdc_22 "vechain", "vechain/test" -> R.drawable.img_vechain_22 + "aptos", "aptos/test" -> R.drawable.img_aptos_22 else -> R.drawable.ic_alert_24 } } @@ -97,6 +98,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "decimal", "decimal/test" -> R.drawable.img_decimal_22 "xdc-network", "xdc-network/test" -> R.drawable.img_xdc_22 "vechain", "vechain/test" -> R.drawable.img_vechain_22 + "aptos", "aptos/test" -> R.drawable.img_aptos_22 else -> R.drawable.ic_alert_24 } } @@ -143,6 +145,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "decimal" -> R.drawable.img_decimal_22 "xdce-crowd-sale" -> R.drawable.img_xdc_22 "vechain" -> R.drawable.img_vechain_22 + "aptos" -> R.drawable.img_aptos_22 else -> R.drawable.ic_alert_24 } } @@ -192,6 +195,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "decimal", "decimal/test" -> R.drawable.ic_decimal_22 "xdc", "xdc/test" -> R.drawable.ic_xdc_22 "vechain", "vechain/test" -> R.drawable.ic_vechain_22 + "aptos", "aptos/test" -> R.drawable.ic_aptos_22 else -> R.drawable.ic_alert_24 } } @@ -241,6 +245,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "decimal", "decimal/test" -> R.drawable.ic_decimal_22 "xdc-network", "xdc-network/test" -> R.drawable.ic_xdc_22 "vechain", "vechain/test" -> R.drawable.ic_vechain_22 + "aptos", "aptos/test" -> R.drawable.ic_aptos_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_aptos_22.xml b/core/ui/src/main/res/drawable/ic_aptos_22.xml new file mode 100644 index 0000000000..5a01778f17 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_aptos_22.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_aptos_22.xml b/core/ui/src/main/res/drawable/img_aptos_22.xml new file mode 100644 index 0000000000..acfa723f51 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_aptos_22.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index fa7d68bd98..fc4296fdb8 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -1,5 +1,6 @@ package com.tangem.data.tokens.repository +import com.tangem.blockchain.common.Blockchain import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.NetworkStatusFactory @@ -7,6 +8,7 @@ import com.tangem.data.tokens.utils.ResponseCryptoCurrenciesFactory import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency @@ -68,6 +70,11 @@ internal class DefaultNetworksRepository( networksStatusesStore.getSyncOrNull(userWalletId).orEmpty() } + override fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean { + val blockchain = Blockchain.fromNetworkId(network.id.value) + return blockchain == Blockchain.Aptos + } + private suspend fun fetchNetworksStatusesIfCacheExpired( userWalletId: UserWalletId, networks: Set, diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 1d03f1f1b3..eb399a3e08 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -225,6 +225,7 @@ fun Blockchain.amountToCreateAccount(token: Token? = null): BigDecimal? { Blockchain.Stellar -> if (token?.symbol == NODL) BigDecimal(NODL_AMOUNT_TO_CREATE_ACCOUNT) else BigDecimal.ONE Blockchain.XRP -> BigDecimal.TEN Blockchain.Near, Blockchain.NearTestnet -> 0.00182.toBigDecimal() + Blockchain.Aptos, Blockchain.AptosTestnet -> BigDecimal.ZERO else -> null } } @@ -239,4 +240,5 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Ducatus, + Blockchain.Aptos, ) \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt index f0acf4eef2..e393e4fbd3 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt @@ -30,6 +30,8 @@ sealed class CryptoCurrencyWarning { val amountCurrency: CryptoCurrency, ) : CryptoCurrencyWarning() + object TopUpWithoutReserve : CryptoCurrencyWarning() + /** * Represents wallet blockchain rent * @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 9599d1a964..b62cccb971 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -251,10 +251,14 @@ class GetCurrencyWarningsUseCase( private fun getNetworkNoAccountWarning(currencyStatus: CryptoCurrencyStatus): CryptoCurrencyWarning? { return (currencyStatus.value as? CryptoCurrencyStatus.NoAccount)?.let { - CryptoCurrencyWarning.SomeNetworksNoAccount( - amountToCreateAccount = it.amountToCreateAccount, - amountCurrency = currencyStatus.currency, - ) + if (networksRepository.isNeedToCreateAccountWithoutReserve(network = currencyStatus.currency.network)) { + CryptoCurrencyWarning.TopUpWithoutReserve + } else { + CryptoCurrencyWarning.SomeNetworksNoAccount( + amountToCreateAccount = it.amountToCreateAccount, + amountCurrency = currencyStatus.currency, + ) + } } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index 8e1b1103ff..16eb05e84a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -43,4 +43,6 @@ interface NetworksRepository { networks: Set, refresh: Boolean, ): Set + + fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index f345dbedeb..ecf1d2a23b 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -32,4 +32,6 @@ internal class MockNetworksRepository( ): Set { return getNetworkStatusesUpdates(userWalletId, networks).first() } + + override fun isNeedToCreateAccountWithoutReserve(network: Network) = false } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index 9c3c74dfd6..3a470f1a30 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -39,6 +39,7 @@ internal class TokenDetailsNotificationsAnalyticsSender( is TokenDetailsNotification.ExistentialDeposit, is TokenDetailsNotification.HasPendingTransactions, is TokenDetailsNotification.NetworksNoAccount, + is TokenDetailsNotification.TopUpWithoutReserve, is TokenDetailsNotification.RentInfo, is TokenDetailsNotification.SwapPromo, -> null diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index e880e6aa3e..8a4732ea16 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -147,6 +147,11 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { ), ) + object TopUpWithoutReserve : Informational( + title = resourceReference(id = R.string.warning_no_account_title), + subtitle = resourceReference(id = R.string.no_account_bnb), + ) + class HasPendingTransactions(val coinSymbol: String) : Informational( title = resourceReference(R.string.warning_send_blocked_pending_transactions_title), subtitle = resourceReference( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index caad6173cc..c2a5ea5591 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification.* import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.removeBy @@ -19,13 +20,13 @@ internal class TokenDetailsNotificationConverter( fun removeRentInfo(currentState: TokenDetailsState): ImmutableList { val newNotifications = currentState.notifications.toMutableList() - newNotifications.removeBy { it is TokenDetailsNotification.RentInfo } + newNotifications.removeBy { it is RentInfo } return newNotifications.toImmutableList() } private fun mapToNotification(warning: CryptoCurrencyWarning): TokenDetailsNotification { return when (warning) { - is CryptoCurrencyWarning.BalanceNotEnoughForFee -> TokenDetailsNotification.NetworkFeeWithBuyButton( + is CryptoCurrencyWarning.BalanceNotEnoughForFee -> NetworkFeeWithBuyButton( currency = warning.tokenCurrency, networkName = warning.coinCurrency.name, feeCurrencyName = warning.coinCurrency.name, @@ -35,7 +36,7 @@ internal class TokenDetailsNotificationConverter( is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> { val feeCurrency = warning.feeCurrency if (feeCurrency != null) { - TokenDetailsNotification.NetworkFeeWithBuyButton( + NetworkFeeWithBuyButton( currency = warning.currency, networkName = feeCurrency.network.name, feeCurrencyName = warning.feeCurrencyName, @@ -43,7 +44,7 @@ internal class TokenDetailsNotificationConverter( onBuyClick = { clickIntents.onBuyCoinClick(feeCurrency) }, ) } else { - TokenDetailsNotification.NetworkFee( + NetworkFee( currency = warning.currency, networkName = warning.networkName, feeCurrencyName = warning.feeCurrencyName, @@ -51,23 +52,22 @@ internal class TokenDetailsNotificationConverter( ) } } - is CryptoCurrencyWarning.ExistentialDeposit -> TokenDetailsNotification.ExistentialDeposit( - existentialInfo = warning, - ) - is CryptoCurrencyWarning.Rent -> TokenDetailsNotification.RentInfo( + is CryptoCurrencyWarning.ExistentialDeposit -> ExistentialDeposit(existentialInfo = warning) + is CryptoCurrencyWarning.Rent -> RentInfo( rentInfo = warning, onCloseClick = clickIntents::onCloseRentInfoNotification, ) - CryptoCurrencyWarning.SomeNetworksUnreachable -> TokenDetailsNotification.NetworksUnreachable - is CryptoCurrencyWarning.SomeNetworksNoAccount -> TokenDetailsNotification.NetworksNoAccount( + CryptoCurrencyWarning.SomeNetworksUnreachable -> NetworksUnreachable + is CryptoCurrencyWarning.SomeNetworksNoAccount -> NetworksNoAccount( network = warning.amountCurrency.name, amount = warning.amountToCreateAccount.toString(), symbol = warning.amountCurrency.symbol, ) - is CryptoCurrencyWarning.HasPendingTransactions -> TokenDetailsNotification.HasPendingTransactions( + is CryptoCurrencyWarning.TopUpWithoutReserve -> TopUpWithoutReserve + is CryptoCurrencyWarning.HasPendingTransactions -> HasPendingTransactions( coinSymbol = warning.blockchainSymbol, ) - is CryptoCurrencyWarning.SwapPromo -> TokenDetailsNotification.SwapPromo( + is CryptoCurrencyWarning.SwapPromo -> SwapPromo( onSwapClick = clickIntents::onSwapPromoClick, onCloseClick = clickIntents::onSwapPromoDismiss, ) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 90d9bdb59f..d9c45fa3cf 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,7 +85,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-463" +tangemBlockchainSdk = "develop-467" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-319" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From cd61d9c841cc2f19e152cfc138532291dd6c3cad Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jan 2024 16:53:00 +0300 Subject: [PATCH 12/15] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 18 ++ ...GetBalanceNotEnoughForFeeWarningUseCase.kt | 97 ++++++++++ .../IsAmountSubtractAvailableUseCase.kt | 29 +++ .../impl/presentation/state/SendUiState.kt | 1 + .../state/fee/FeeNotificationFactory.kt | 112 ++++++++---- .../presentation/state/fee/FeeStateFactory.kt | 166 +++++++++--------- .../state/fee/SendFeeStateConverter.kt | 1 + .../ui/fee/SendSpeedAndFeeContent.kt | 31 +++- .../presentation/viewmodel/SendViewModel.kt | 57 ++++-- 9 files changed, 369 insertions(+), 143 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 336c1129ed..b439d53019 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -310,4 +310,22 @@ internal object TokensDomainModule { ): GetNetworksSupportedByWallet { return GetNetworksSupportedByWallet(repository = repository) } + + @Provides + @ViewModelScoped + fun provideGetBalanceNotEnoughForFeeWarningUseCase( + currenciesRepository: CurrenciesRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetBalanceNotEnoughForFeeWarningUseCase { + return GetBalanceNotEnoughForFeeWarningUseCase(currenciesRepository, dispatchers) + } + + @Provides + @ViewModelScoped + fun provideIsAmountSubtractAvailableUseCase( + currenciesRepository: CurrenciesRepository, + dispatchers: CoroutineDispatcherProvider, + ): IsAmountSubtractAvailableUseCase { + return IsAmountSubtractAvailableUseCase(currenciesRepository, dispatchers) + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt new file mode 100644 index 0000000000..b1f054e498 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt @@ -0,0 +1,97 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.FeePaidCurrency +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +/** + * Use case for getting balance not enough warning to cover fee. + * + * This warning is shown when current currency is not paying fee and paying fee currency balance is not enough + * + * Current | Paying fee | Warning + * Coin | Coin | - + * Token | Coin | + + * Coin | PToken | + (VTO - VTHO) + * Token | PToken | + (Other VeChainToken - VTHO) + * PToken | PToken | - (VTHO - VTHO or TerraToken - TerraToken) + */ +class GetBalanceNotEnoughForFeeWarningUseCase( + private val currenciesRepository: CurrenciesRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + suspend operator fun invoke( + fee: BigDecimal, + userWalletId: UserWalletId, + tokenStatus: CryptoCurrencyStatus, + coinStatus: CryptoCurrencyStatus, + ): Either = Either.catch { + withContext(dispatchers.io) { + val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, tokenStatus.currency) + val coinBalance = coinStatus.value.amount ?: BigDecimal.ZERO + + val isFeePaidByCoin = tokenStatus.currency is CryptoCurrency.Token + val isFeePaidByToken = + feePaidCurrency is FeePaidCurrency.Token && tokenStatus.currency.id != feePaidCurrency.tokenId + + val warning = when { + feePaidCurrency is FeePaidCurrency.Coin && isFeePaidByCoin && fee > coinBalance -> { + CryptoCurrencyWarning.BalanceNotEnoughForFee( + tokenCurrency = tokenStatus.currency, + coinCurrency = coinStatus.currency, + ) + } + feePaidCurrency is FeePaidCurrency.Token && isFeePaidByToken && fee > feePaidCurrency.balance -> { + constructTokenBalanceNotEnoughWarning( + userWalletId = userWalletId, + tokenStatus = tokenStatus, + feePaidToken = feePaidCurrency, + ) + } + else -> null + } + warning + } + } + + /** + * Check if fee paying token [feePaidToken] is added to wallet [userWalletId] + */ + private suspend fun constructTokenBalanceNotEnoughWarning( + userWalletId: UserWalletId, + tokenStatus: CryptoCurrencyStatus, + feePaidToken: FeePaidCurrency.Token, + ): CryptoCurrencyWarning { + val token = currenciesRepository + .getMultiCurrencyWalletCurrenciesSync(userWalletId) + .find { + it is CryptoCurrency.Token && + it.contractAddress.equals(feePaidToken.contractAddress, ignoreCase = true) && + it.network.derivationPath == tokenStatus.currency.network.derivationPath + } + return if (token != null) { + CryptoCurrencyWarning.CustomTokenNotEnoughForFee( + currency = tokenStatus.currency, + feeCurrency = token, + networkName = token.network.name, + feeCurrencyName = feePaidToken.name, + feeCurrencySymbol = feePaidToken.symbol, + ) + } else { + CryptoCurrencyWarning.CustomTokenNotEnoughForFee( + currency = tokenStatus.currency, + feeCurrency = null, + networkName = tokenStatus.currency.network.name, + feeCurrencyName = feePaidToken.name, + feeCurrencySymbol = feePaidToken.symbol, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt new file mode 100644 index 0000000000..d003a95067 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.FeePaidCurrency +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +/** + * Use case for checking if currency amount can be subtracted. + * Amount can be subtracted if only it is paying fee + */ +class IsAmountSubtractAvailableUseCase( + private val currenciesRepository: CurrenciesRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either = + Either.catch { + withContext(dispatchers.io) { + when (val feeCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, currency)) { + is FeePaidCurrency.Coin -> currency is CryptoCurrency.Coin + is FeePaidCurrency.SameCurrency -> true + is FeePaidCurrency.Token -> currency.id == feeCurrency.tokenId + } + } + } +} \ 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 ea8245e94c..4407d17d0f 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 @@ -68,6 +68,7 @@ internal sealed class SendStates { override val type: SendUiStateType = SendUiStateType.Fee, override val isPrimaryButtonEnabled: Boolean = false, val feeSelectorState: FeeSelectorState, + val isSubtractAvailable: Boolean, val isSubtract: Boolean, val isUserSubtracted: Boolean, val fee: Fee?, 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 8f51e6164a..c434a6182f 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 @@ -3,39 +3,53 @@ package com.tangem.features.send.impl.presentation.state.fee import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider -import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import java.math.BigDecimal internal class FeeNotificationFactory( private val coinCryptoCurrencyStatusProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, + private val currentStateProvider: Provider, private val userWalletProvider: Provider, private val clickIntents: SendClickIntents, + private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, ) { - operator fun invoke(feeState: SendStates.FeeState, amountValue: BigDecimal?): ImmutableList = - buildList { - when (val feeSelectorState = feeState.feeSelectorState) { - FeeSelectorState.Loading -> Unit - FeeSelectorState.Error -> { - addFeeUnreachableNotification(feeSelectorState) + fun create() = currentStateProvider().currentState + .filter { it == SendUiStateType.Fee } + .map { + val state = currentStateProvider() + val feeState = state.feeState ?: return@map persistentListOf() + buildList { + when (val feeSelectorState = feeState.feeSelectorState) { + FeeSelectorState.Loading -> Unit + FeeSelectorState.Error -> { + addFeeUnreachableNotification(feeSelectorState) + } + is FeeSelectorState.Content -> { + val customFee = feeSelectorState.customValues + val selectedFee = feeSelectorState.selectedFee + addTooLowNotification(feeSelectorState.fees, selectedFee, customFee) + addTooHighNotification(feeSelectorState.fees, selectedFee, customFee) + addFeeCoverageNotification(feeState, state.amountState) + addExceedsBalanceNotification(feeState.fee) + } } - is FeeSelectorState.Content -> { - val customFee = feeSelectorState.customValues - val selectedFee = feeSelectorState.selectedFee - addTooLowNotification(feeSelectorState.fees, selectedFee, customFee) - addTooHighNotification(feeSelectorState.fees, selectedFee, customFee) - addFeeCoverageNotification(feeState, amountValue) - addExceedsBalanceNotification(feeState.fee) - } - } - }.toImmutableList() + }.toImmutableList() + } private fun MutableList.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) { if (feeSelectorState is FeeSelectorState.Error) { @@ -72,32 +86,60 @@ internal class FeeNotificationFactory( private fun MutableList.addFeeCoverageNotification( feeState: SendStates.FeeState, - amountValue: BigDecimal?, + amountState: SendStates.AmountState?, ) { - val cryptoAmount = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO - val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO - val value = amountValue ?: BigDecimal.ZERO + if (!feeState.isSubtractAvailable) return + + val cryptoAmount = coinCryptoCurrencyStatusProvider().value.amount ?: return + val feeValue = feeState.fee?.amount?.value ?: return + val value = amountState?.amountTextField?.cryptoAmount?.value ?: return if (cryptoAmount <= value + feeValue && feeState.isSubtract && !feeState.isUserSubtracted) { add(SendFeeNotification.Warning.NetworkCoverage) } } - private fun MutableList.addExceedsBalanceNotification(fee: Fee?) { - val coinCryptoCurrency = coinCryptoCurrencyStatusProvider() - val cryptoAmount = coinCryptoCurrency.value.amount ?: BigDecimal.ZERO + private suspend fun MutableList.addExceedsBalanceNotification(fee: Fee?) { val feeValue = fee?.amount?.value ?: BigDecimal.ZERO + val userWalletId = userWalletProvider().walletId + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - if (feeValue > cryptoAmount) { - add( - SendFeeNotification.Error.ExceedsBalance( - coinCryptoCurrency.currency.networkIconResId, - ) { - clickIntents.onTokenDetailsClick( - userWalletProvider().walletId, - coinCryptoCurrency.currency, - ) - }, - ) + val warning = getBalanceNotEnoughForFeeWarningUseCase( + fee = feeValue, + userWalletId = userWalletId, + tokenStatus = cryptoCurrencyStatus, + coinStatus = coinCryptoCurrencyStatusProvider(), + ).fold( + ifLeft = { null }, + ifRight = { it }, + ) ?: return + + when (warning) { + is CryptoCurrencyWarning.BalanceNotEnoughForFee -> { + add( + SendFeeNotification.Error.ExceedsBalance( + warning.coinCurrency.networkIconResId, + ) { + clickIntents.onTokenDetailsClick( + userWalletProvider().walletId, + warning.coinCurrency, + ) + }, + ) + } + is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> { + val currency = warning.feeCurrency ?: warning.currency + add( + SendFeeNotification.Error.ExceedsBalance( + currency.networkIconResId, + ) { + clickIntents.onTokenDetailsClick( + userWalletId, + currency, + ) + }, + ) + } + else -> Unit } } 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 index 8a1073cad3..dd003ef56f 100644 --- 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 @@ -5,12 +5,12 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import com.tangem.utils.isNullOrZero +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal @@ -24,7 +24,6 @@ internal class FeeStateFactory( private val coinCryptoCurrencyStatusProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, private val appCurrencyProvider: Provider, - userWalletProvider: Provider, ) { private val customFeeFieldConverter by lazy { SendFeeCustomFieldConverter( @@ -33,12 +32,6 @@ internal class FeeStateFactory( ) } - private val feeNotificationFactory = FeeNotificationFactory( - coinCryptoCurrencyStatusProvider = coinCryptoCurrencyStatusProvider, - userWalletProvider = userWalletProvider, - clickIntents = clickIntents, - ) - fun onFeeOnLoadingState(): SendUiState { val state = currentStateProvider() val feeState = state.feeState ?: return state @@ -46,12 +39,12 @@ internal class FeeStateFactory( feeState = feeState.copy( feeSelectorState = FeeSelectorState.Loading, notifications = persistentListOf(), - isPrimaryButtonEnabled = feeState.isPrimaryButtonEnabled(), + isPrimaryButtonEnabled = false, ), ) } - fun onFeeOnLoadedState(fees: TransactionFee): SendUiState { + fun onFeeOnLoadedState(fees: TransactionFee, isSubtractAvailable: Boolean): SendUiState { val state = currentStateProvider() val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO val feeState = state.feeState ?: return state @@ -65,35 +58,46 @@ internal class FeeStateFactory( val fee = feeSelectorState.getFee() val receivedAmount = calculateReceiveAmount(state, fee) - val updatedState = feeState.copy( - feeSelectorState = feeSelectorState, - fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - isSubtract = checkAutoSubtract(state, fee, balance), - ) return state.copy( - feeState = updatedState.copy( - notifications = feeNotificationFactory( - feeState = updatedState, - amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, - ), - isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), + feeState = feeState.copy( + isSubtractAvailable = isSubtractAvailable, + feeSelectorState = feeSelectorState, + fee = fee, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = isSubtractAvailable && checkAutoSubtract(state, fee, balance), + ), + ) + } + + fun onFeeOnLoadedState(fees: TransactionFee): SendUiState { + val state = currentStateProvider() + val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val feeState = state.feeState ?: return state + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state + + val updatedFeeSelector = feeSelectorState.copy( + fees = fees, + customValues = customFeeFieldConverter.convert(fees.normal), + ) + val fee = updatedFeeSelector.getFee() + val receivedAmount = calculateReceiveAmount(state, fee) + return state.copy( + feeState = feeState.copy( + feeSelectorState = updatedFeeSelector, + fee = fee, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = checkAutoSubtract(state, fee, balance), ), ) } fun onFeeOnErrorState(): SendUiState { val state = currentStateProvider() - val updatedState = state.feeState?.copy( - feeSelectorState = FeeSelectorState.Error, - ) return state.copy( - feeState = updatedState?.copy( - notifications = feeNotificationFactory( - feeState = updatedState, - amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, - ), + feeState = state.feeState?.copy( + feeSelectorState = FeeSelectorState.Error, ), ) } @@ -107,21 +111,13 @@ internal class FeeStateFactory( val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType) val fee = updatedFeeSelectorState.getFee() val receivedAmount = calculateReceiveAmount(state, fee) - val updatedState = feeState.copy( - fee = fee, - feeSelectorState = updatedFeeSelectorState, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - isSubtract = checkAutoSubtract(state, fee, balance), - ) - return state.copy( - feeState = updatedState.copy( - notifications = feeNotificationFactory( - feeState = updatedState, - amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, - ), - isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), + feeState = feeState.copy( + fee = fee, + feeSelectorState = updatedFeeSelectorState, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = checkAutoSubtract(state, fee, balance), ), ) } @@ -139,20 +135,13 @@ internal class FeeStateFactory( val fee = updatedFeeSelectorState.getFee() val receivedAmount = calculateReceiveAmount(state, fee) - val updatedState = feeState.copy( - feeSelectorState = updatedFeeSelectorState, - fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - isSubtract = checkAutoSubtract(state, fee, balance), - ) return state.copy( - feeState = updatedState.copy( - notifications = feeNotificationFactory( - feeState = updatedState, - amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, - ), - isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), + feeState = feeState.copy( + feeSelectorState = updatedFeeSelectorState, + fee = fee, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = checkAutoSubtract(state, fee, balance), ), ) } @@ -163,42 +152,47 @@ internal class FeeStateFactory( val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state val fee = feeSelectorState.getFee() val receivedAmount = calculateReceiveAmount(state, fee) - val updatedState = feeState.copy( - isSubtract = value, - isUserSubtracted = true, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - fee = fee, - ) return state.copy( - feeState = updatedState.copy( - notifications = feeNotificationFactory( - feeState = updatedState, - amountValue = state.amountState?.amountTextField?.cryptoAmount?.value, - ), - isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(), + feeState = feeState.copy( + isSubtract = value, + isUserSubtracted = true, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + fee = fee, ), ) } - private fun SendStates.FeeState.isPrimaryButtonEnabled(): Boolean { - return when (feeSelectorState) { - is FeeSelectorState.Content -> { - val customValue = feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull() - val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO - val fee = feeSelectorState.getFee() - val feeValue = fee.amount.value ?: BigDecimal.ZERO + fun getFeeNotificationState(notifications: ImmutableList): SendUiState { + val state = currentStateProvider() + return state.copy( + feeState = state.feeState?.copy( + notifications = notifications, + isPrimaryButtonEnabled = isPrimaryButtonEnabled(state.feeState, notifications), + ), + ) + } - val isNotCustom = feeSelectorState.selectedFee != FeeType.CUSTOM - val isNotEmptyCustom = !customValue.isNullOrZero() && !isNotCustom - val isFiatAnotherCurrency = cryptoCurrencyStatusProvider().currency.symbol != fee.amount.currencySymbol - val isSubtractRequired = if (feeValue + receivedAmountValue >= balance) isSubtract else true - val isBalanceEnough = feeValue + receivedAmountValue <= balance + private fun isPrimaryButtonEnabled( + feeState: SendStates.FeeState, + notifications: ImmutableList, + ): Boolean { + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return false + val customValue = feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull() + val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val fee = feeSelectorState.getFee() + val feeValue = fee.amount.value ?: BigDecimal.ZERO - (isFiatAnotherCurrency || isBalanceEnough && isSubtractRequired) && (isNotEmptyCustom || isNotCustom) - } - else -> false + val isNotCustom = feeSelectorState.selectedFee != FeeType.CUSTOM + val isNotEmptyCustom = !customValue.isNullOrZero() && !isNotCustom + val noErrors = notifications.none { it is SendFeeNotification.Error } + val isSubtractRequired = when { + !feeState.isSubtractAvailable -> true // current currency is not fee currency + feeValue + feeState.receivedAmountValue >= balance -> feeState.isSubtract + else -> feeValue + feeState.receivedAmountValue <= balance } + + return noErrors && isSubtractRequired && (isNotEmptyCustom || isNotCustom) } private fun checkAutoSubtract(state: SendUiState, fee: Fee, balance: BigDecimal): Boolean { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt index ccd5f4ae89..312ed32c90 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -10,6 +10,7 @@ internal class SendFeeStateConverter : Converter { override fun convert(value: Unit): SendStates.FeeState { return SendStates.FeeState( feeSelectorState = FeeSelectorState.Loading, + isSubtractAvailable = false, isSubtract = false, isUserSubtracted = false, fee = null, 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 c490f4b84b..43ab51b3ac 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 @@ -22,6 +22,7 @@ import kotlinx.collections.immutable.ImmutableList private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY" private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY" +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: SendClickIntents) { if (state == null) return @@ -41,6 +42,7 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S SendSpeedSelector( state = feeSendState, clickIntents = clickIntents, + modifier = Modifier.animateItemPlacement(), ) } customFee(feeSendState) @@ -48,6 +50,7 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S subtractButton( receivedAmount = state.receivedAmount, isSubtract = state.isSubtract, + isSubtractAvailable = state.isSubtractAvailable, clickIntents = clickIntents, ) } @@ -73,6 +76,13 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.icon.accent + is SendFeeNotification.Error.ExceedsBalance -> { + if (it.config.buttonsState == null) { + TangemTheme.colors.icon.warning + } else { + null + } + } else -> null }, ) @@ -108,17 +118,20 @@ internal fun LazyListScope.customFee(feeSendState: FeeSelectorState, modifier: M internal fun LazyListScope.subtractButton( receivedAmount: String, isSubtract: Boolean, + isSubtractAvailable: Boolean, clickIntents: SendClickIntents, modifier: Modifier = Modifier, ) { - item { - SendSpeedSubtract( - receivingAmount = receivedAmount, - isSubtract = isSubtract, - onSelectClick = clickIntents::onSubtractSelect, - modifier = modifier - .padding(vertical = TangemTheme.dimens.spacing12) - .animateItemPlacement(), - ) + if (isSubtractAvailable) { + item { + SendSpeedSubtract( + receivingAmount = receivedAmount, + isSubtract = isSubtract, + onSelectClick = clickIntents::onSubtractSelect, + modifier = modifier + .padding(vertical = TangemTheme.dimens.spacing12) + .animateItemPlacement(), + ) + } } } \ 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 0f5ede3fe2..cbdd0688e0 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 @@ -17,10 +17,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase -import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase -import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase +import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.utils.convertToAmount @@ -40,10 +37,7 @@ import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.* -import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState -import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory -import com.tangem.features.send.impl.presentation.state.fee.FeeType -import com.tangem.features.send.impl.presentation.state.fee.getFee +import com.tangem.features.send.impl.presentation.state.fee.* import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel @@ -73,8 +67,10 @@ internal class SendViewModel @Inject constructor( private val parseSharedAddressUseCase: ParseSharedAddressUseCase, private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, + private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, validateWalletMemoUseCase: ValidateWalletMemoUseCase, + getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { @@ -106,7 +102,6 @@ internal class SendViewModel @Inject constructor( coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), - userWalletProvider = Provider { userWallet }, ) private val eventStateFactory = SendEventStateFactory( @@ -115,6 +110,15 @@ internal class SendViewModel @Inject constructor( feeStateFactory = feeStateFactory, ) + private val feeNotificationFactory = FeeNotificationFactory( + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, + currentStateProvider = Provider { uiState }, + userWalletProvider = Provider { userWallet }, + clickIntents = this, + getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase, + ) + private val sendNotificationFactory = SendNotificationFactory( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, @@ -129,6 +133,7 @@ internal class SendViewModel @Inject constructor( private set private var userWallet: UserWallet by Delegates.notNull() + private var isAmountSubtractAvailable: Boolean = false private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() @@ -137,6 +142,7 @@ internal class SendViewModel @Inject constructor( private var feeJobHolder = JobHolder() private var addressValidationJobHolder = JobHolder() private var sendNotificationsJobHolder = JobHolder() + private var feeNotificationsJobHolder = JobHolder() private var qrScannerJobHolder = JobHolder() private var checkFeeUpdateScheduler = SingleTaskScheduler?>() @@ -163,6 +169,7 @@ internal class SendViewModel @Inject constructor( getUserWalletUseCase(userWalletId).fold( ifRight = { wallet -> userWallet = wallet + checkIfSubtractAvailable() getCurrenciesStatusUpdates(owner, wallet) }, ifLeft = { @@ -326,6 +333,16 @@ internal class SendViewModel @Inject constructor( .saveIn(sendNotificationsJobHolder) } + private fun updateFeeNotifications() { + feeNotificationFactory.create() + .conflate() + .distinctUntilChanged() + .onEach { uiState = feeStateFactory.getFeeNotificationState(notifications = it) } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(feeNotificationsJobHolder) + } + // region screen state navigation override fun popBackStack() = stateRouter.popBackStack() override fun onBackClick() = stateRouter.onBackClick() @@ -429,30 +446,41 @@ internal class SendViewModel @Inject constructor( override fun onFeeSelectorClick(feeType: FeeType) { uiState = feeStateFactory.onFeeSelectedState(feeType) + updateFeeNotifications() } override fun onCustomFeeValueChange(index: Int, value: String) { uiState = feeStateFactory.onCustomFeeValueChange(index, value) + updateFeeNotifications() } override fun onSubtractSelect(value: Boolean) { uiState = feeStateFactory.onSubtractSelect(value) + updateFeeNotifications() } private fun loadFee() { viewModelScope.launch(dispatchers.main) { uiState = feeStateFactory.onFeeOnLoadingState() uiState = callFeeUseCase()?.fold( - ifRight = { - feeStateFactory.onFeeOnLoadedState(it) + ifRight = { fees -> + feeStateFactory.onFeeOnLoadedState(fees, isAmountSubtractAvailable) }, ifLeft = { feeStateFactory.onFeeOnErrorState() }, ) ?: feeStateFactory.onFeeOnErrorState() + updateFeeNotifications() }.saveIn(feeJobHolder) } + private suspend fun checkIfSubtractAvailable() { + isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, cryptoCurrency).fold( + ifRight = { it }, + ifLeft = { false }, + ) + } + private suspend fun callFeeUseCase(): Either? { val amountState = uiState.amountState ?: return null val recipientState = uiState.recipientState ?: return null @@ -501,8 +529,11 @@ internal class SendViewModel @Inject constructor( val memo = uiState.recipientState?.memoTextField?.value val fee = feeSelectorState.getFee() val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return - - val amountToSend = if (feeState.isSubtract) feeState.receivedAmountValue else amountValue + val amountToSend = if (feeState.isSubtract && isAmountSubtractAvailable) { + feeState.receivedAmountValue + } else { + amountValue + } viewModelScope.launch(dispatchers.main) { createTransactionUseCase( From 07bf6c376c4d09f2a30400f143dc4950436585d2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jan 2024 12:13:47 +0300 Subject: [PATCH 13/15] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 4 - .../handlers/BuyCurrencyIntentHandler.kt | 27 --- .../handlers/SellCurrencyIntentHandler.kt | 44 ----- .../middlewares/TradeCryptoMiddleware.kt | 69 ++++---- core/deep-links/global/.gitignore | 1 + core/deep-links/global/build.gradle.kts | 15 ++ .../deeplink/global/BuyCurrencyDeepLink.kt | 12 ++ .../deeplink/global/SellCurrencyDeepLink.kt | 24 +++ .../GetCryptoCurrencyStatusSyncUseCase.kt | 20 ++- .../domain/tokens/legacy/TradeCryptoAction.kt | 70 ++++---- .../CurrenciesStatusesOperations.kt | 23 +++ .../tokens/repository/NetworksRepository.kt | 2 +- features/tokendetails/impl/build.gradle.kts | 2 + .../viewmodels/TokenDetailsViewModel.kt | 59 +++++-- features/wallet/impl/build.gradle.kts | 2 + .../deeplink/WalletDeepLinksHandler.kt | 156 ++++++++++++++++++ .../wallet/viewmodels/WalletViewModel.kt | 12 +- .../wallet/viewmodels/WalletViewModelV2.kt | 7 + .../WalletCurrencyActionsClickIntents.kt | 10 +- settings.gradle.kts | 1 + 20 files changed, 386 insertions(+), 174 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt create mode 100644 core/deep-links/global/.gitignore create mode 100644 core/deep-links/global/build.gradle.kts create mode 100644 core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt create mode 100644 core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 74f347ee5f..4ef519edbd 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -58,8 +58,6 @@ import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsL import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.features.intentHandler.IntentProcessor import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler -import com.tangem.tap.features.intentHandler.handlers.BuyCurrencyIntentHandler -import com.tangem.tap.features.intentHandler.handlers.SellCurrencyIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.features.main.model.Toast @@ -301,8 +299,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac val hasSavedWalletsProvider = { store.state.globalState.userWalletsListManager?.hasUserWallets == true } intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope)) intentProcessor.addHandler(WalletConnectLinkIntentHandler()) - intentProcessor.addHandler(BuyCurrencyIntentHandler()) - intentProcessor.addHandler(SellCurrencyIntentHandler()) } private fun updateAppTheme(appThemeMode: AppThemeMode) { diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt deleted file mode 100644 index 31a7657c79..0000000000 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.tap.features.intentHandler.handlers - -import android.content.Intent -import com.tangem.tap.features.intentHandler.IntentHandler - -/** -[REDACTED_AUTHOR] - */ -class BuyCurrencyIntentHandler : IntentHandler { - - override fun handleIntent(intent: Intent?): Boolean { - // FIXME: [REDACTED_JIRA] - // val data = intent?.data ?: return false - // val currency = store.state.walletState.selectedCurrency ?: return false - // - // val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL) - // return if (data.host == successUri.host && data.authority == successUri.authority) { - // val currencyType = AnalyticsParam.CurrencyType.Currency(currency) - // Analytics.send(TokenScreenAnalyticsEvent.Bought(currencyType.value)) - // true - // } else { - // false - // } - - return false - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt deleted file mode 100644 index 52e8e12a33..0000000000 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.tap.features.intentHandler.handlers - -import android.content.Intent -import com.tangem.tap.features.intentHandler.IntentHandler - -/** -[REDACTED_AUTHOR] - */ -class SellCurrencyIntentHandler : IntentHandler { - - override fun handleIntent(intent: Intent?): Boolean { - // FIXME: [REDACTED_JIRA] - // return try { - // val intentData = intent?.data ?: return false - // val transactionID = intentData.getQueryParameter(TRANSACTION_ID_PARAM) ?: return false - // val currency = intentData.getQueryParameter(CURRENCY_CODE_PARAM) ?: return false - // val amount = intentData.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return false - // val destinationAddress = intentData.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return false - // - // Timber.d("MoonPay Sell: $amount $currency to $destinationAddress") - // store.dispatchOnMain( - // TradeCryptoAction.SendCrypto( - // currencyId = currency, - // amount = amount, - // destinationAddress = destinationAddress, - // transactionId = transactionID, - // ), - // ) - // true - // } catch (exception: Exception) { - // Timber.d("Not MoonPay URL") - // false - // } - - return false - } - - // private companion object { - // private const val TRANSACTION_ID_PARAM = "transactionId" - // private const val CURRENCY_CODE_PARAM = "baseCurrencyCode" - // private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount" - // private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress" - // } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index b1db79189b..36123cf1a9 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -22,6 +22,7 @@ import com.tangem.tap.domain.TapError import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.send.redux.PrepareSendScreen +import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens import com.tangem.tap.proxy.redux.DaggerGraphState @@ -50,19 +51,18 @@ object TradeCryptoMiddleware { if (DemoHelper.tryHandle(state, action)) return when (action) { - is TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen() is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) - is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action) - is TradeCryptoAction.New.Sell -> proceedNewSellAction(action) - is TradeCryptoAction.New.Swap -> openSwap( + is TradeCryptoAction.Buy -> proceedBuyAction(state, action) + is TradeCryptoAction.Sell -> proceedSellAction(action) + is TradeCryptoAction.Swap -> openSwap( currency = action.cryptoCurrency, ) - is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action) - is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action) + is TradeCryptoAction.SendToken -> handleSendToken(action = action) + is TradeCryptoAction.SendCoin -> handleSendCoin(action = action) } } - private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) { + private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { val networkAddress = action.cryptoCurrencyStatus.value.networkAddress ?.defaultAddress ?.let(NetworkAddress.Address::value) @@ -119,7 +119,7 @@ object TradeCryptoMiddleware { } } - private fun proceedNewSellAction(action: TradeCryptoAction.New.Sell) { + private fun proceedSellAction(action: TradeCryptoAction.Sell) { val networkAddress = action.cryptoCurrencyStatus.value.networkAddress ?.defaultAddress ?.let(NetworkAddress.Address::value) @@ -139,33 +139,6 @@ object TradeCryptoMiddleware { } } - private fun preconfigureAndOpenSendScreen() = scope.launch { - // FIXME: [REDACTED_JIRA] - // val selectedWalletData = store.state.walletState.selectedWalletData ?: return - // - // Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(selectedWalletData.currency))) - // val walletManager = store.state.walletState.getWalletManager(selectedWalletData.currency).guard { - // FirebaseCrashlytics.getInstance().recordException(IllegalStateException("WalletManager is null")) - // return - // } - // - // store.dispatchOnMain( - // PrepareSendScreen( - // walletManager = walletManager, - // coinAmount = walletManager.wallet.amounts[AmountType.Coin], - // coinRate = selectedWalletData.fiatRate, - // ), - // ) - // store.dispatchOnMain( - // SendAction.SendSpecificTransaction( - // sendAmount = action.amount, - // destinationAddress = action.destinationAddress, - // transactionId = action.transactionId, - // ), - // ) - // store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send)) - } - private fun openReceiptUrl(transactionId: String) { store.dispatchOnMain(NavigationAction.PopBackTo()) store.state.globalState.exchangeManager.getSellCryptoReceiptUrl( @@ -182,7 +155,7 @@ object TradeCryptoMiddleware { store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) } - private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) { + private fun handleSendToken(action: TradeCryptoAction.SendToken) { val currency = action.tokenCurrency val blockchain = Blockchain.fromId(currency.network.id.value) @@ -221,6 +194,17 @@ object TradeCryptoMiddleware { ), ) + val txInfo = action.transactionInfo + if (txInfo != null) { + store.dispatchOnMain( + SendAction.SendSpecificTransaction( + sendAmount = txInfo.amount, + destinationAddress = txInfo.destinationAddress, + transactionId = txInfo.transactionId, + ), + ) + } + val bundle = bundleOf( SendRouter.CRYPTO_CURRENCY_KEY to currency, SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, @@ -229,7 +213,7 @@ object TradeCryptoMiddleware { } } - private fun handleNewSendCoin(action: TradeCryptoAction.New.SendCoin) { + private fun handleSendCoin(action: TradeCryptoAction.SendCoin) { val cryptoStatus = action.coinStatus val currency = cryptoStatus.currency val blockchain = Blockchain.fromId(currency.network.id.value) @@ -278,6 +262,17 @@ object TradeCryptoMiddleware { is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token") } + val txInfo = action.transactionInfo + if (txInfo != null) { + store.dispatchOnMain( + SendAction.SendSpecificTransaction( + sendAmount = txInfo.amount, + destinationAddress = txInfo.destinationAddress, + transactionId = txInfo.transactionId, + ), + ) + } + val bundle = bundleOf( SendRouter.CRYPTO_CURRENCY_KEY to currency, SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, diff --git a/core/deep-links/global/.gitignore b/core/deep-links/global/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/core/deep-links/global/.gitignore @@ -0,0 +1 @@ +/build diff --git a/core/deep-links/global/build.gradle.kts b/core/deep-links/global/build.gradle.kts new file mode 100644 index 0000000000..ac47c5ac3d --- /dev/null +++ b/core/deep-links/global/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.core.deeplink.global" +} + +dependencies { + + /* Project */ + implementation(projects.core.deepLinks) +} \ No newline at end of file diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt new file mode 100644 index 0000000000..286f8f8c73 --- /dev/null +++ b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt @@ -0,0 +1,12 @@ +package com.tangem.core.deeplink.global + +import com.tangem.core.deeplink.DeepLink + +class BuyCurrencyDeepLink(val onReceive: () -> Unit) : DeepLink { + + override val uri: String = "tangem://success.tangem.com" + + override fun onReceive(params: Map) { + onReceive() + } +} \ No newline at end of file diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt new file mode 100644 index 0000000000..5d9047a83b --- /dev/null +++ b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt @@ -0,0 +1,24 @@ +package com.tangem.core.deeplink.global + +import com.tangem.core.deeplink.DeepLink + +class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink { + + override val uri: String = "tangem://sell-request.tangem.com" + + override fun onReceive(params: Map) { + val data = Data( + transactionId = params["transactionId"] ?: return, + baseCurrencyAmount = params["baseCurrencyAmount"] ?: return, + depositWalletAddress = params["depositWalletAddress"] ?: return, + ) + + onReceive(data) + } + + data class Data( + val transactionId: String, + val baseCurrencyAmount: String, + val depositWalletAddress: String, + ) +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt index ffe6e79c3b..ed3cbfad6c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt @@ -1,8 +1,8 @@ package com.tangem.domain.tokens import arrow.core.Either -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.error.mapper.mapToTokenListError +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations @@ -22,7 +22,7 @@ class GetCryptoCurrencyStatusSyncUseCase( suspend operator fun invoke( userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, - ): Either { + ): Either { val operations = CurrenciesStatusesOperations( userWalletId = userWalletId, currenciesRepository = currenciesRepository, @@ -31,6 +31,18 @@ class GetCryptoCurrencyStatusSyncUseCase( ) return operations.getCurrencyStatusSync(cryptoCurrencyId) - .mapLeft { error -> error.mapToTokenListError() } + .mapLeft { error -> error.mapToCurrencyError() } + } + + suspend operator fun invoke(userWalletId: UserWalletId): Either { + val operations = CurrenciesStatusesOperations( + userWalletId = userWalletId, + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + ) + + return operations.getPrimaryCurrencyStatusSync() + .mapLeft { error -> error.mapToCurrencyError() } } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index 85abbb2bc8..1657a3ea05 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -8,43 +8,41 @@ import java.math.BigDecimal sealed class TradeCryptoAction : Action { - data class SendCrypto( - val currencyId: String, + data class FinishSelling(val transactionId: String) : TradeCryptoAction() + + data class Buy( + val userWallet: UserWallet, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val appCurrencyCode: String, + val checkUserLocation: Boolean = true, + ) : TradeCryptoAction() + + data class Sell( + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val appCurrencyCode: String, + ) : TradeCryptoAction() + + data class SendToken( + val userWallet: UserWallet, + val tokenCurrency: CryptoCurrency.Token, + val tokenFiatRate: BigDecimal?, + val coinFiatRate: BigDecimal?, + val feeCurrencyStatus: CryptoCurrencyStatus?, + val transactionInfo: TransactionInfo? = null, + ) : TradeCryptoAction() + + data class SendCoin( + val userWallet: UserWallet, + val coinStatus: CryptoCurrencyStatus, + val feeCurrencyStatus: CryptoCurrencyStatus?, + val transactionInfo: TransactionInfo? = null, + ) : TradeCryptoAction() + + data class Swap(val cryptoCurrency: CryptoCurrency) : TradeCryptoAction() + + data class TransactionInfo( val amount: String, val destinationAddress: String, val transactionId: String, - ) : TradeCryptoAction() - - data class FinishSelling(val transactionId: String) : TradeCryptoAction() - - sealed class New : TradeCryptoAction() { - - data class Buy( - val userWallet: UserWallet, - val cryptoCurrencyStatus: CryptoCurrencyStatus, - val appCurrencyCode: String, - val checkUserLocation: Boolean = true, - ) : New() - - data class Sell( - val cryptoCurrencyStatus: CryptoCurrencyStatus, - val appCurrencyCode: String, - ) : New() - - data class SendToken( - val userWallet: UserWallet, - val tokenCurrency: CryptoCurrency.Token, - val tokenFiatRate: BigDecimal?, - val coinFiatRate: BigDecimal?, - val feeCurrencyStatus: CryptoCurrencyStatus?, - ) : New() - - data class SendCoin( - val userWallet: UserWallet, - val coinStatus: CryptoCurrencyStatus, - val feeCurrencyStatus: CryptoCurrencyStatus?, - ) : New() - - data class Swap(val cryptoCurrency: CryptoCurrency) : New() - } + ) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index a86698763b..92d6969de0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -11,6 +11,8 @@ import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* +// FIXME: Refactor - [REDACTED_JIRA] +@Suppress("LargeClass") internal class CurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, @@ -107,6 +109,27 @@ internal class CurrenciesStatusesOperations( } } + suspend fun getPrimaryCurrencyStatusSync(): Either = either { + val currency = catch( + block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }, + catch = { raise(Error.DataError(it)) }, + ) + val quotes = catch( + block = { quotesRepository.getQuoteSync(currency.id).right() }, + catch = { Error.DataError(it).left() }, + ) + val networkStatus = catch( + block = { + networksRepository.getNetworkStatusesSync(userWalletId, setOf(currency.network)) + .firstOrNull { it.network == currency.network } + .right() + }, + catch = { Error.DataError(it).left() }, + ) + + return createCurrencyStatus(currency, quotes, networkStatus) + } + fun getCardCurrenciesStatusesFlow(): Flow>> { return flow { val nonEmptyCurrencies = recover( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index 16eb05e84a..6ec3fbb11a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -41,7 +41,7 @@ interface NetworksRepository { suspend fun getNetworkStatusesSync( userWalletId: UserWalletId, networks: Set, - refresh: Boolean, + refresh: Boolean = false, ): Set fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 860b64eec4..81f6868424 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -51,6 +51,8 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.datasource) + implementation(projects.core.deepLinks) + implementation(projects.core.deepLinks.global) /** Domain modules */ implementation(projects.domain.appCurrency) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 7a7d53ba09..1fba010ecc 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -9,6 +9,9 @@ import arrow.core.getOrElse import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.AddressType import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.deeplink.DeepLinksRegistry +import com.tangem.core.deeplink.global.BuyCurrencyDeepLink +import com.tangem.core.deeplink.global.SellCurrencyDeepLink import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.resourceReference @@ -22,6 +25,7 @@ import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress @@ -87,6 +91,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val isDemoCardUseCase: IsDemoCardUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, + deepLinksRegistry: DeepLinksRegistry, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { @@ -148,6 +153,34 @@ internal class TokenDetailsViewModel @Inject constructor( var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(cryptoCurrency)) private set + init { + deepLinksRegistry.registerWithViewModel( + viewModel = this, + deepLinks = listOf( + BuyCurrencyDeepLink(::onBuyCurrencyDeepLink), + SellCurrencyDeepLink(::onSellCurrencyDeepLink), + ), + ) + } + + private fun onBuyCurrencyDeepLink() { + val currency = cryptoCurrencyStatus?.currency ?: return + analyticsEventsHandler.send(TokenScreenAnalyticsEvent.Bought(currency.symbol)) + } + + private fun onSellCurrencyDeepLink(data: SellCurrencyDeepLink.Data) { + sendCurrency( + status = cryptoCurrencyStatus ?: return, + transactionInfo = data.let { + TransactionInfo( + amount = it.baseCurrencyAmount, + transactionId = it.transactionId, + destinationAddress = it.depositWalletAddress, + ) + }, + ) + } + override fun onCreate(owner: LifecycleOwner) { analyticsEventsHandler.send( event = TokenScreenAnalyticsEvent.DetailsScreenOpened(token = cryptoCurrency.symbol), @@ -330,7 +363,7 @@ internal class TokenDetailsViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { reduxStateHolder.dispatch( - TradeCryptoAction.New.Buy( + TradeCryptoAction.Buy( userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }, cryptoCurrencyStatus = status, appCurrencyCode = selectedAppCurrencyFlow.value.code, @@ -354,27 +387,31 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onSendClick() { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonSend(cryptoCurrency.symbol)) - val cryptoCurrencyStatus = cryptoCurrencyStatus ?: return + sendCurrency(status = cryptoCurrencyStatus ?: return) + } + private fun sendCurrency(status: CryptoCurrencyStatus, transactionInfo: TransactionInfo? = null) { viewModelScope.launch(dispatchers.main) { val maybeFeeCurrencyStatus = - getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyStatus).getOrNull() + getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() - when (val currency = cryptoCurrencyStatus.currency) { + when (val currency = status.currency) { is CryptoCurrency.Coin -> { reduxStateHolder.dispatch( - action = TradeCryptoAction.New.SendCoin( + action = TradeCryptoAction.SendCoin( userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }, - coinStatus = cryptoCurrencyStatus, + coinStatus = status, feeCurrencyStatus = maybeFeeCurrencyStatus, + transactionInfo = transactionInfo, ), ) } is CryptoCurrency.Token -> { sendToken( tokenCurrency = currency, - tokenFiatRate = cryptoCurrencyStatus.value.fiatRate, + tokenFiatRate = status.value.fiatRate, feeCurrencyStatus = maybeFeeCurrencyStatus, + transactionInfo = transactionInfo, ) } } @@ -385,6 +422,7 @@ internal class TokenDetailsViewModel @Inject constructor( tokenCurrency: CryptoCurrency.Token, tokenFiatRate: BigDecimal?, feeCurrencyStatus: CryptoCurrencyStatus?, + transactionInfo: TransactionInfo?, ) { viewModelScope.launch(dispatchers.io) { val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } @@ -399,7 +437,7 @@ internal class TokenDetailsViewModel @Inject constructor( .firstOrNull() reduxStateHolder.dispatchWithMain( - action = TradeCryptoAction.New.SendToken( + action = TradeCryptoAction.SendToken( userWallet = wallet, tokenCurrency = tokenCurrency, tokenFiatRate = tokenFiatRate, @@ -408,6 +446,7 @@ internal class TokenDetailsViewModel @Inject constructor( ifRight = { it.value.fiatRate }, ), feeCurrencyStatus = feeCurrencyStatus, + transactionInfo = transactionInfo, ), ) } @@ -440,7 +479,7 @@ internal class TokenDetailsViewModel @Inject constructor( val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse reduxStateHolder.dispatch( - TradeCryptoAction.New.Sell( + TradeCryptoAction.Sell( cryptoCurrencyStatus = status, appCurrencyCode = selectedAppCurrencyFlow.value.code, ), @@ -451,7 +490,7 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onSwapClick() { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrency.symbol)) - reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrency)) + reduxStateHolder.dispatch(TradeCryptoAction.Swap(cryptoCurrency)) } override fun onDismissDialog() { diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index a3207eb5cd..decfa1c37e 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -49,6 +49,8 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.common) + implementation(projects.core.deepLinks) + implementation(projects.core.deepLinks.global) /** Domain modules */ implementation(projects.domain.card) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt new file mode 100644 index 0000000000..0248645642 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt @@ -0,0 +1,156 @@ +package com.tangem.feature.wallet.presentation.deeplink + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.deeplink.DeepLink +import com.tangem.core.deeplink.DeepLinksRegistry +import com.tangem.core.deeplink.global.BuyCurrencyDeepLink +import com.tangem.core.deeplink.global.SellCurrencyDeepLink +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyUseCase +import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +internal class WalletDeepLinksHandler @Inject constructor( + private val deepLinksRegistry: DeepLinksRegistry, + private val analyticsEventHandler: AnalyticsEventHandler, + private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, + private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, + private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, + private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, + private val reduxStateHolder: ReduxStateHolder, +) { + + private var deepLinks: List = emptyList() + + fun registerForSingleCurrencyWallets(viewModel: ViewModel, userWallet: UserWallet) { + if (userWallet.isMultiCurrency) { + deepLinksRegistry.unregister(deepLinks) + } else { + if (deepLinks.isEmpty()) { + deepLinks = getDeepLinks(userWallet, viewModel.viewModelScope) + } + + deepLinksRegistry.register(deepLinks) + } + + viewModel.addCloseable { + deepLinksRegistry.unregister(deepLinks) + } + } + + private fun getDeepLinks(userWallet: UserWallet, scope: CoroutineScope): List { + val sellCurrencyDeepLink = SellCurrencyDeepLink( + onReceive = { data -> + scope.launch { + onSellCurrencyDeepLink(userWallet, data) + } + }, + ) + val buyCurrencyDeepLink = BuyCurrencyDeepLink( + onReceive = { + scope.launch { + onBuyCurrencyDeepLink(userWallet) + } + }, + ) + + return listOf(sellCurrencyDeepLink, buyCurrencyDeepLink) + } + + private suspend fun onSellCurrencyDeepLink(userWallet: UserWallet, data: SellCurrencyDeepLink.Data) { + val cryptoCurrencyStatus = getCryptoCurrencyStatusSyncUseCase(userWallet.walletId) + .getOrNull() ?: return + val feeCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWallet.walletId, + cryptoCurrencyStatus, + ).getOrNull() + + val transactionInfo = data.let { + TransactionInfo( + amount = it.baseCurrencyAmount, + destinationAddress = it.depositWalletAddress, + transactionId = it.transactionId, + ) + } + + when (cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> sendCoin( + userWallet = userWallet, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCurrencyStatus, + transactionInfo = transactionInfo, + ) + is CryptoCurrency.Token -> sendToken( + userWallet = userWallet, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCurrencyStatus, + transactionInfo = transactionInfo, + ) + } + } + + private suspend fun onBuyCurrencyDeepLink(userWallet: UserWallet) { + val cryptoCurrency = getCryptoCurrencyUseCase(userWallet.walletId).getOrNull() ?: return + + analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol)) + } + + private fun sendCoin( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, + transactionInfo: TransactionInfo, + ) { + reduxStateHolder.dispatch( + action = TradeCryptoAction.SendCoin( + userWallet = userWallet, + coinStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCurrencyStatus, + transactionInfo = transactionInfo, + ), + ) + } + + private suspend fun sendToken( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, + transactionInfo: TransactionInfo, + ) { + val cryptoCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return + val coinStatus = getNetworkCoinStatusUseCase( + userWalletId = userWallet.walletId, + networkId = cryptoCurrency.network.id, + derivationPath = cryptoCurrency.network.derivationPath, + isSingleWalletWithTokens = false, + ) + .firstOrNull() + ?.getOrNull() + ?: return + + reduxStateHolder.dispatch( + action = TradeCryptoAction.SendToken( + userWallet = userWallet, + tokenCurrency = cryptoCurrency, + tokenFiatRate = cryptoCurrencyStatus.value.fiatRate, + coinFiatRate = coinStatus.value.fiatRate, + feeCurrencyStatus = feeCurrencyStatus, + transactionInfo = transactionInfo, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 065a47b196..8adfe05590 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -609,7 +609,7 @@ internal class WalletViewModel @Inject constructor( showErrorIfDemoModeOrElse { reduxStateHolder.dispatch( - TradeCryptoAction.New.Buy( + TradeCryptoAction.Buy( userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex), cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrencyCode = selectedAppCurrencyFlow.value.code, @@ -623,7 +623,7 @@ internal class WalletViewModel @Inject constructor( event = TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrencyStatus.currency.symbol), ) - reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrencyStatus.currency)) + reduxStateHolder.dispatch(TradeCryptoAction.Swap(cryptoCurrencyStatus.currency)) } override fun onSingleCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus?) { @@ -641,7 +641,7 @@ internal class WalletViewModel @Inject constructor( val maybeFeeCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(userWallet.walletId, coinStatus).getOrNull() reduxStateHolder.dispatch( - action = TradeCryptoAction.New.SendCoin( + action = TradeCryptoAction.SendCoin( userWallet = userWallet, coinStatus = coinStatus, feeCurrencyStatus = maybeFeeCurrencyStatus, @@ -665,7 +665,7 @@ internal class WalletViewModel @Inject constructor( is CryptoCurrency.Coin -> { uiState = stateFactory.getStateWithClosedBottomSheet() reduxStateHolder.dispatch( - action = TradeCryptoAction.New.SendCoin( + action = TradeCryptoAction.SendCoin( userWallet = userWallet, coinStatus = cryptoCurrencyStatus, feeCurrencyStatus = maybeFeeCurrencyStatus, @@ -694,7 +694,7 @@ internal class WalletViewModel @Inject constructor( it.onRight { coinStatus -> uiState = stateFactory.getStateWithClosedBottomSheet() reduxStateHolder.dispatchWithMain( - action = TradeCryptoAction.New.SendToken( + action = TradeCryptoAction.SendToken( userWallet = userWallet, tokenCurrency = requireNotNull(cryptoCurrencyStatus.currency as? CryptoCurrency.Token), tokenFiatRate = cryptoCurrencyStatus.value.fiatRate, @@ -772,7 +772,7 @@ internal class WalletViewModel @Inject constructor( showErrorIfDemoModeOrElse { reduxStateHolder.dispatch( - action = TradeCryptoAction.New.Sell( + action = TradeCryptoAction.Sell( cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrencyCode = selectedAppCurrencyFlow.value.code, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt index ae097c7991..418da4487e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt @@ -13,6 +13,7 @@ import com.tangem.domain.walletconnect.WalletConnectActions import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase +import com.tangem.feature.wallet.presentation.deeplink.WalletDeepLinksHandler import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender @@ -57,6 +58,7 @@ internal class WalletViewModelV2 @Inject constructor( private val reduxStateHolder: ReduxStateHolder, private val screenLifecycleProvider: ScreenLifecycleProvider, private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender, + private val walletDeepLinksHandler: WalletDeepLinksHandler, ) : ViewModel() { val uiState: StateFlow = stateHolder.uiState @@ -157,6 +159,11 @@ internal class WalletViewModelV2 @Inject constructor( selectedWalletAnalyticsSender.send(selectedWallet) } + + walletDeepLinksHandler.registerForSingleCurrencyWallets( + viewModel = this, + userWallet = selectedWallet, + ) } .flowOn(dispatchers.main) .launchIn(viewModelScope) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 3fd26c242b..198af9b84d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -110,7 +110,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( feeCurrencyStatus: CryptoCurrencyStatus?, ) { reduxStateHolder.dispatch( - action = TradeCryptoAction.New.SendCoin( + action = TradeCryptoAction.SendCoin( userWallet = userWallet, coinStatus = cryptoCurrencyStatus, feeCurrencyStatus = feeCurrencyStatus, @@ -135,7 +135,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( .collectLatest { it.onRight { coinStatus -> reduxStateHolder.dispatch( - action = TradeCryptoAction.New.SendToken( + action = TradeCryptoAction.SendToken( userWallet = userWallet, tokenCurrency = cryptoCurrency, tokenFiatRate = cryptoCurrencyStatus.fiatRate, @@ -281,7 +281,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showErrorIfDemoModeOrElse { viewModelScope.launch(dispatchers.main) { reduxStateHolder.dispatch( - action = TradeCryptoAction.New.Sell( + action = TradeCryptoAction.Sell( cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, ), @@ -300,7 +300,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showErrorIfDemoModeOrElse { viewModelScope.launch(dispatchers.main) { reduxStateHolder.dispatch( - TradeCryptoAction.New.Buy( + TradeCryptoAction.Buy( userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, @@ -315,7 +315,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( event = TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrencyStatus.currency.symbol), ) - reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrencyStatus.currency)) + reduxStateHolder.dispatch(TradeCryptoAction.Swap(cryptoCurrencyStatus.currency)) } override fun onExploreClick() { diff --git a/settings.gradle.kts b/settings.gradle.kts index 7060d410f2..3ac4a3dcf4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -71,6 +71,7 @@ include(":core:res") include(":core:ui") include(":core:utils") include(":core:deep-links") +include(":core:deep-links:global") // endregion Core modules // region Libs modules From 11f62a51bc047ed21e9b3e98a8e91a60f3930f83 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jan 2024 11:11:59 +0100 Subject: [PATCH 14/15] Updated on 2026-08-14 --- .../models/response/ExpressErrorResponse.kt | 3 +++ core/res/src/main/res/values-ru/strings.xml | 4 +++- core/res/src/main/res/values/strings.xml | 4 +++- .../swap/converters/ErrorsDataConverter.kt | 13 ++++++++++- .../feature/swap/domain/models/DataError.kt | 2 ++ .../tangem/feature/swap/ui/ProviderItem.kt | 4 ++-- .../tangem/feature/swap/ui/StateBuilder.kt | 22 ++++++++++++++++++- .../feature/swap/viewmodels/SwapViewModel.kt | 7 +++--- 8 files changed, 50 insertions(+), 9 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt index ea8a552360..d3e2fd0694 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt @@ -23,6 +23,9 @@ data class ExpressErrorValue( @Json(name = "minAmount") val minAmount: String?, + @Json(name = "maxAmount") + val maxAmount: String?, + @Json(name = "decimals") val decimals: Int?, diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index efdc1b3d26..d5cb28c7c0 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -232,6 +232,7 @@ Провайдер Лучший курс Доступно с %s + Доступно до %s Недоступно для этой пары Требуется разрешение Условиями использования @@ -693,8 +694,9 @@ Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s Невозможно покрыть комиссию %s Cервис временно недоступен - Пожалуйста, измените сумму для обмена + Пожалуйста, измените сумму для обмена Сумма для обмена должна быть не менее %s + Сумма для обмена должна быть не более %s Возможно, данная карта - образец или подделка Ошибка проверки подлинности На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 125fb77911..82870064e4 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -235,6 +235,7 @@ Provider Best rate Available from %s + Available up to %s Unavailable for this pair Permission Required Terms of Use @@ -709,8 +710,9 @@ To make a transaction you need to deposit some %1$s %2$s Unable to cover %s fee Service temporarily unavailable - Please change the amount to swap + Please change the amount to swap The amount to swap must be at least %s + The amount of tokens to be swapped must not exceed %s This card might be a production sample or counterfeit Authenticity check failed Only %s signatures are left on this card. You must withdraw all of your funds. diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt index fa28cf44f2..dca890c55e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt @@ -11,7 +11,7 @@ internal class ErrorsDataConverter( private val jsonAdapter: JsonAdapter, ) : Converter { - @Suppress("MagicNumber") + @Suppress("MagicNumber", "CyclomaticComplexMethod") override fun convert(value: String): DataError { try { val error = jsonAdapter.fromJson(value)?.error ?: return DataError.UnknownError @@ -23,6 +23,7 @@ internal class ErrorsDataConverter( 2230 -> DataError.ExchangeProviderNotAvailableError(code = error.code) 2240 -> DataError.ExchangeNotPossibleError(code = error.code) 2250 -> tryParseExchangeTooSmallAmountError(error = error) + 2251 -> tryParseExchangeTooBigAmountError(error = error) 2260 -> tryParseExchangeNotEnoughAllowanceError(error = error) 2270 -> DataError.ExchangeNotEnoughBalanceError(code = error.code) 2280 -> DataError.ExchangeInvalidAddressError(code = error.code) @@ -44,6 +45,16 @@ internal class ErrorsDataConverter( ) } + private fun tryParseExchangeTooBigAmountError(error: ExpressError): DataError { + val minAmount = error.value?.maxAmount ?: return DataError.UnknownErrorWithCode(error.code) + val decimals = error.value?.decimals ?: return DataError.UnknownErrorWithCode(error.code) + + return DataError.ExchangeTooBigAmountError( + code = error.code, + amount = createFromAmountWithOffset(minAmount, decimals), + ) + } + private fun tryParseExchangeNotEnoughAllowanceError(error: ExpressError): DataError { val currentAllowance = error.value?.currentAllowance ?: return DataError.UnknownErrorWithCode(error.code) diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt index 0da33b0bcf..21c40c5588 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt @@ -18,6 +18,8 @@ sealed class DataError { data class ExchangeTooSmallAmountError(override val code: Int, val amount: SwapAmount) : DataError() + data class ExchangeTooBigAmountError(override val code: Int, val amount: SwapAmount) : DataError() + data class ExchangeNotEnoughAllowanceError(override val code: Int, val currentAllowance: BigDecimal) : DataError() data class ExchangeNotEnoughBalanceError(override val code: Int) : DataError() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 931563da97..eaddec7e0c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -146,7 +146,7 @@ private fun ProviderContentState( } Row( modifier = Modifier.padding( - top = TangemTheme.dimens.spacing8, + top = TangemTheme.dimens.spacing6, end = TangemTheme.dimens.spacing56, ), ) { @@ -238,7 +238,7 @@ private fun ProviderUnavailableState( text = it.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + modifier = Modifier.padding(top = TangemTheme.dimens.spacing6), ) } } 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 76f84dc0e2..d6a880f213 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 @@ -534,6 +534,16 @@ internal class StateBuilder( onProviderClick = onProviderClick, ) } + is DataError.ExchangeTooBigAmountError -> { + swapProvider.convertToAvailableFromProviderState( + alertText = resourceReference( + R.string.express_provider_max_amount, + wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), + ), + selectionType = selectionType, + onProviderClick = onProviderClick, + ) + } else -> { ProviderState.Empty() } @@ -548,7 +558,17 @@ internal class StateBuilder( id = R.string.warning_express_too_minimal_amount_title, formatArgs = wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), ), - subtitle = resourceReference(R.string.warning_express_too_minimal_amount_description), + subtitle = resourceReference(R.string.warning_express_wrong_amount_description), + iconResId = R.drawable.ic_alert_circle_24, + ), + ) + is DataError.ExchangeTooBigAmountError -> SwapWarning.GeneralError( + notificationConfig = NotificationConfig( + title = resourceReference( + id = R.string.warning_express_too_maximum_amount_title, + formatArgs = wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), + ), + subtitle = resourceReference(R.string.warning_express_wrong_amount_description), iconResId = R.drawable.ic_alert_circle_24, ), ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index c95f3ed693..ace4cb9006 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -99,8 +99,9 @@ internal class SwapViewModel @Inject constructor( private val lastAmount = mutableStateOf(INITIAL_AMOUNT) private var swapRouter: SwapRouter by Delegates.notNull() - private val isExchangeTooSmallAmountError: (SwapState) -> Boolean = { - it is SwapState.SwapError && it.error is DataError.ExchangeTooSmallAmountError + private val isUserResolvableError: (SwapState) -> Boolean = { + it is SwapState.SwapError && + (it.error is DataError.ExchangeTooSmallAmountError || it.error is DataError.ExchangeTooBigAmountError) } val currentScreen: SwapNavScreen @@ -1001,7 +1002,7 @@ internal class SwapViewModel @Inject constructor( private fun Map.consideredProvidersStates(): Map { return this.filter { - it.value is SwapState.QuotesLoadedState || isExchangeTooSmallAmountError(it.value) + it.value is SwapState.QuotesLoadedState || isUserResolvableError(it.value) } } From 9c925b309adcb9d2dbe402132140ebb8e9f0c6db Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jan 2024 13:17:57 +0300 Subject: [PATCH 15/15] Updated on 2026-08-14 --- .../send/impl/navigation/DefaultSendRouter.kt | 1 + .../state/SendEventStateFactory.kt | 6 +- .../impl/presentation/state/StateRouter.kt | 25 ++-- .../ui/recipient/ListItemWithIcon.kt | 4 +- .../ui/recipient/SendRecipientContent.kt | 1 - .../ui/recipient/TextFieldWithPaste.kt | 3 +- .../presentation/viewmodel/SendViewModel.kt | 107 ++++++++---------- 7 files changed, 65 insertions(+), 82 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index 360165f228..db243390ea 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -23,6 +23,7 @@ internal class DefaultSendRouter( } override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { + reduxNavController.popBackStack() reduxNavController.navigate( action = NavigationAction.NavigateTo( screen = AppScreen.WalletDetails, 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 index 794f00706a..5ecb7dc55c 100644 --- 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 @@ -4,8 +4,9 @@ import com.tangem.blockchain.common.transaction.TransactionFee 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.state.fee.* import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory +import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.fee.getFee import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider @@ -41,7 +42,7 @@ internal class SendEventStateFactory( ) } - fun getFeeUpdatedAlert(fee: TransactionFee, onConsume: () -> Unit): SendUiState { + fun getFeeUpdatedAlert(fee: TransactionFee, onConsume: () -> Unit, onFeeNotIncreased: () -> Unit): SendUiState { val state = currentStateProvider() val feeSelector = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state val newFee = when (fee) { @@ -67,6 +68,7 @@ internal class SendEventStateFactory( ), ) } else { + onFeeNotIncreased() updateFeeState } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index 97ad5f2dfc..4edb445226 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -11,22 +11,16 @@ internal class StateRouter( var currentState: MutableStateFlow = MutableStateFlow(SendUiStateType.Recipient) private set - private var isFromSend: Boolean = false - fun popBackStack() { fragmentManager.get()?.popBackStack() } fun onBackClick() { - if (isFromSend) { - showSend() - } else { - when (currentState.value) { - SendUiStateType.Recipient -> popBackStack() - SendUiStateType.Amount -> showRecipient() - SendUiStateType.Fee -> showAmount() - SendUiStateType.Send -> showFee() - } + when (currentState.value) { + SendUiStateType.Recipient -> popBackStack() + SendUiStateType.Amount -> showRecipient() + SendUiStateType.Fee -> showAmount() + SendUiStateType.Send -> showFee() } } @@ -48,18 +42,15 @@ internal class StateRouter( } } - fun showAmount(isFromSend: Boolean = false) { - this.isFromSend = isFromSend + fun showAmount() { currentState.update { SendUiStateType.Amount } } - fun showRecipient(isFromSend: Boolean = false) { - this.isFromSend = isFromSend + fun showRecipient() { currentState.update { SendUiStateType.Recipient } } - fun showFee(isFromSend: Boolean = false) { - this.isFromSend = isFromSend + fun showFee() { currentState.update { SendUiStateType.Fee } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt index e83ba769bd..d16846ae48 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt @@ -24,6 +24,7 @@ import androidx.constraintlayout.compose.Visibility import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R @@ -49,10 +50,11 @@ fun ListItemWithIcon( subtitleEndOffset: Int = 0, @DrawableRes subtitleIconRes: Int? = null, ) { + val hapticFeedback = rememberHapticFeedback(state = title, onAction = onClick) ConstraintLayout( modifier = modifier .fillMaxWidth() - .clickable { onClick() } + .clickable { hapticFeedback() } .padding(horizontal = TangemTheme.dimens.spacing12), ) { val (iconRef, titleRef, subtitleRef, subtitleIconRef) = createRefs() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index 57187dffc1..d2741dffcc 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -59,7 +59,6 @@ internal fun SendRecipientContent( placeholder = address.placeholder, onValueChange = address.onValueChange, onPasteClick = clickIntents::onRecipientAddressValueChange, - singleLine = true, isError = isError, isLoading = isValidating, error = address.error, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt index b01980e9e9..758fef6f57 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.ui.recipient import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -55,7 +56,7 @@ internal fun TextFieldWithPaste( placeholder = placeholder, onValueChange = onValueChange, modifier = Modifier - .weight(1f) + .fillMaxWidth() .padding(top = TangemTheme.dimens.spacing6), ) } 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 cbdd0688e0..32629e642a 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 @@ -39,7 +39,9 @@ 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.* import com.tangem.utils.Provider -import com.tangem.utils.coroutines.* +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @@ -145,17 +147,11 @@ internal class SendViewModel @Inject constructor( private var feeNotificationsJobHolder = JobHolder() private var qrScannerJobHolder = JobHolder() - private var checkFeeUpdateScheduler = SingleTaskScheduler?>() + private var sendIdleTimer = 0L override fun onCreate(owner: LifecycleOwner) { subscribeOnCurrencyStatusUpdates(owner) - onFeeStateActive() - onCheckFeeUpdate() - } - - override fun onStop(owner: LifecycleOwner) { - super.onStop(owner) - checkFeeUpdateScheduler.cancelTask() + onStateActive() } fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) { @@ -316,10 +312,15 @@ internal class SendViewModel @Inject constructor( } } - private fun onFeeStateActive() { + private fun onStateActive() { uiState.currentState - .filter { it == SendUiStateType.Fee } - .onEach { loadFee() } + .onEach { + when (it) { + SendUiStateType.Fee -> loadFee() + SendUiStateType.Send -> sendIdleTimer = System.currentTimeMillis() + else -> Unit + } + } .launchIn(viewModelScope) } @@ -501,14 +502,19 @@ internal class SendViewModel @Inject constructor( if (sendState.isSuccess) popBackStack() uiState = stateFactory.getSendingStateUpdate(isSending = true) - verifyAndSendTransaction() + if (System.currentTimeMillis() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) { + verifyAndSendTransaction() + } else { + onCheckFeeUpdate() + } + sendIdleTimer = System.currentTimeMillis() } - override fun showAmount() = stateRouter.showAmount(isFromSend = true) + override fun showAmount() = stateRouter.showAmount() - override fun showRecipient() = stateRouter.showRecipient(isFromSend = true) + override fun showRecipient() = stateRouter.showRecipient() - override fun showFee() = stateRouter.showFee(isFromSend = true) + override fun showFee() = stateRouter.showFee() override fun onExploreClick(txUrl: String) = innerRouter.openUrl(txUrl) @@ -591,63 +597,44 @@ internal class SendViewModel @Inject constructor( } private fun onCheckFeeUpdate() { - uiState.currentState - .onEach { activeState -> - val isSending = uiState.sendState.isSending - val isSuccess = uiState.sendState.isSuccess - val isNoNotifications = uiState.sendState.notifications.isEmpty() - val isSendIdle = !isSending && !isSuccess && isNoNotifications - if (activeState == SendUiStateType.Send && isSendIdle) { - checkFeeUpdateScheduler.scheduleTask( - scope = viewModelScope, - task = getFeePeriodicTask(), - ) - } else { - checkFeeUpdateScheduler.cancelTask() - } - } - .launchIn(viewModelScope) - } + val isSending = uiState.sendState.isSending + val isSuccess = uiState.sendState.isSuccess + val noErrorNotifications = uiState.sendState.notifications.none { it is SendNotification.Error } - private fun getFeePeriodicTask() = PeriodicTask( - delay = CHECK_FEE_UPDATE_DELAY, - isDelayFirst = true, - task = { - runCatching(dispatchers.main) { - if (uiState.sendState.isSuccess) { - checkFeeUpdateScheduler.cancelTask() - null - } else { - callFeeUseCase() - } - } - }, - onSuccess = { maybeFee -> - if (maybeFee == null) { - checkFeeUpdateScheduler.cancelTask() - uiState = eventStateFactory.getGenericErrorState( - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, - ) - } else { - maybeFee.fold( + if (!isSending && !isSuccess && noErrorNotifications) { + viewModelScope.launch(dispatchers.main) { + val feeUpdatedState = callFeeUseCase()?.fold( ifRight = { - uiState = eventStateFactory.getFeeUpdatedAlert( + uiState = stateFactory.getSendingStateUpdate(isSending = false) + eventStateFactory.getFeeUpdatedAlert( fee = it, onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + onFeeNotIncreased = { + uiState = stateFactory.getSendingStateUpdate(isSending = true) + verifyAndSendTransaction() + }, ) }, ifLeft = { - checkFeeUpdateScheduler.cancelTask() - uiState = eventStateFactory.getGenericErrorState( + uiState = stateFactory.getSendingStateUpdate(isSending = false) + eventStateFactory.getGenericErrorState( error = (it as? GetFeeError.DataError)?.cause, onConsume = { uiState = eventStateFactory.onConsumeEventState() }, ) }, ) + + uiState = if (feeUpdatedState != null) { + feeUpdatedState + } else { + uiState = stateFactory.getSendingStateUpdate(isSending = false) + eventStateFactory.getGenericErrorState( + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + } } - }, - onError = { /* no-op */ }, - ) + } + } // endregion companion object {