From 0b037f2b2da7d7da9d7d9233eb583ae80babb1e5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Jan 2024 16:20:14 +0300 Subject: [PATCH 01/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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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≪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 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/28] 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/28] 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/28] 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/28] 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/28] 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 fb5ffe03f8cd6d501b1c8e808cd49aebb49923cb Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 28 Jan 2024 15:02:24 +0300 Subject: [PATCH 12/28] Updated on 2026-08-14 --- .../main/java/com/tangem/domain/common/extensions/Blockchain.kt | 1 + 1 file changed, 1 insertion(+) 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..7d235349dc 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 @@ -239,4 +239,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 From fda885c071647e5b68c800d480b7890a58fa1f7e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jan 2024 12:06:06 +0300 Subject: [PATCH 13/28] Updated on 2026-08-14 --- .../tokens/impl/data/converters/CoinsResponseConverter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt index 0902f40ec4..e2824e1022 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt @@ -3,7 +3,6 @@ package com.tangem.tap.features.tokens.impl.data.converters import com.tangem.blockchain.common.Blockchain import com.tangem.datasource.api.tangemTech.models.CoinsResponse import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.tap.features.tokens.impl.domain.models.Token import com.tangem.utils.converter.Converter @@ -26,7 +25,8 @@ internal object CoinsResponseConverter : Converter> { networks = token.networks.mapNotNull { network -> val blockchain = Blockchain.fromNetworkId(network.networkId) ?: return@mapNotNull null - if (network.networkId != blockchain.toNetworkId() && + // filter tokens, if contractAddress != null, assume that it is a token + if (network.contractAddress != null && !blockchain.canHandleTokens() ) { return@mapNotNull null From d06fc4fb6fdbd4b56e89f0c7833a8367a5a9e2b5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jan 2024 15:52:50 +0300 Subject: [PATCH 14/28] Updated on 2026-08-14 --- .../exchangeServices/mercuryo/MercuryoApi.kt | 22 ++--- .../mercuryo/MercuryoService.kt | 90 +++++++++---------- 2 files changed, 52 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt index 5d9fece041..95f2c90b4c 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt @@ -22,20 +22,16 @@ data class MercuryoCurrenciesResponse( ) data class Config( - val base: Map, - @Json(name = "has_withdrawal_fee") - val hasWithdrawalFee: Map, - @Json(name = "display_options") - val displayOptions: Map, - val icons: Map, + @Json(name = "crypto_currencies") + val cryptoCurrencies: List, ) - data class DisplayOption( - @Json(name = "fullname") - val fullName: String, - @Json(name = "total_digits") - val totalDigits: Int, - @Json(name = "display_digits") - val displayDigits: Int, + data class MercuryoCryptoCurrency( + @Json(name = "currency") + val currencySymbol: String, + @Json(name = "network") + val network: String, + @Json(name = "contract") + val contractAddress: String, ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt index 9446f5c5cf..a19ca0cd84 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -11,7 +11,6 @@ import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder -import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList /** @@ -21,8 +20,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E private val api: MercuryoApi = environment.mercuryoApi - private val blockchainsAvailableToBuy = CopyOnWriteArrayList() - private val tokensAvailableToBuy = ConcurrentHashMap>() + private val availableMercuryoCurrencies = CopyOnWriteArrayList() override fun featureIsSwitchedOn(): Boolean = true @@ -33,29 +31,14 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E override fun availableForBuy(currency: Currency): Boolean { if (!isBuyAllowed()) return false - // blockchains which cant be defined by mercuryo service - val unsupportedBlockchains = listOf( - Blockchain.Unknown, - Blockchain.Binance, - Blockchain.Arbitrum, - Blockchain.Optimism, - ) - val blockchain = currency.blockchain - - return when (currency) { - is Currency.Blockchain -> { - when { - blockchain.isTestnet() -> blockchain.getTestnetTopUpUrl() != null - unsupportedBlockchains.contains(blockchain) -> false - else -> blockchainsAvailableToBuy.contains(blockchain) - } - } - - is Currency.Token -> { - val supportedInBlockchains = tokensAvailableToBuy[currency.currencySymbol] ?: return false - supportedInBlockchains.contains(blockchain) - } + val mercuryoNetwork = currency.blockchain.mercuryoNetwork() + val contractAddress = (currency as? Currency.Token)?.token?.contractAddress ?: "" + val availableCurrency = availableMercuryoCurrencies.firstOrNull { + it.currencySymbol == currency.currencySymbol && + it.network == mercuryoNetwork && + it.contractAddress == contractAddress } + return availableCurrency != null } override fun availableForSell(currency: Currency): Boolean = false @@ -67,8 +50,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E handleSuccessfullyUpdatedData(data = result.data.data) } result is Result.Failure -> { - blockchainsAvailableToBuy.clear() - tokensAvailableToBuy.clear() + availableMercuryoCurrencies.clear() } } } @@ -95,34 +77,48 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E .appendQueryParameter("return_url", ExchangeUrlBuilder.SUCCESS_URL) if (isDarkTheme) builder.appendQueryParameter("theme", "1inch") + blockchain.mercuryoNetwork()?.let { + builder.appendQueryParameter("network", it) + } + return builder.build().toString() } override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? = null private fun handleSuccessfullyUpdatedData(data: MercuryoCurrenciesResponse.Data) { - data.crypto.forEach { currencyName -> - val blockchain = blockchainFromCurrencyName(currencyName) - if (blockchain == null) { - val specificBlockchain = data.config.base[currencyName]?.let(::blockchainFromCurrencyName) - if (specificBlockchain != null) { - tokensAvailableToBuy.set( - key = currencyName, - value = tokensAvailableToBuy[currencyName].orEmpty() + specificBlockchain, - ) - } - } else { - blockchainsAvailableToBuy.add(blockchain) - } - } + availableMercuryoCurrencies.clear() + availableMercuryoCurrencies.addAll(data.config.cryptoCurrencies) } - private fun blockchainFromCurrencyName(currencyName: String): Blockchain? { - return when (currencyName) { - "BNB" -> Blockchain.BSC - "ETH" -> Blockchain.Ethereum - "ADA" -> Blockchain.Cardano - else -> Blockchain.values().find { it.currency.lowercase() == currencyName.lowercase() } + @Suppress("CyclomaticComplexMethod") + private fun Blockchain.mercuryoNetwork(): String? { + return when (this) { + // Blockchain.Algorand -> "ALGORAND" //TODO: Uncomment with algo support + Blockchain.Arbitrum -> "ARBITRUM" + Blockchain.Avalanche -> "AVALANCHE" + Blockchain.BSC -> "BINANCESMARTCHAIN" + Blockchain.Bitcoin -> "BITCOIN" + Blockchain.BitcoinCash -> "BITCOINCASH" + Blockchain.Cardano -> "CARDANO" + Blockchain.Cosmos -> "COSMOS" + Blockchain.Dash -> "DASH" + Blockchain.Dogecoin -> "DOGECOIN" + Blockchain.Ethereum -> "ETHEREUM" + Blockchain.Fantom -> "FANTOM" + Blockchain.Kusama -> "KUSAMA" + Blockchain.Litecoin -> "LITECOIN" + Blockchain.Near -> "NEAR_PROTOCOL" + Blockchain.TON -> "NEWTON" + Blockchain.Optimism -> "OPTIMISM" + Blockchain.Polkadot -> "POLKADOT" + Blockchain.Polygon -> "POLYGON" + Blockchain.XRP -> "RIPPLE" + Blockchain.Solana -> "SOLANA" + Blockchain.Stellar -> "STELLAR" + Blockchain.Tezos -> "TEZOS" + Blockchain.Tron -> "TRON" + else -> null } } From cd61d9c841cc2f19e152cfc138532291dd6c3cad Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jan 2024 16:53:00 +0300 Subject: [PATCH 15/28] 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 16/28] 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 17/28] 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 18/28] 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 { From 9cd37f92c5e299c70c1b43a72abd832a33aebb0c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jan 2024 14:05:23 +0300 Subject: [PATCH 19/28] Updated on 2026-08-14 --- .../components/SettingsSwitchItem.kt | 3 +- .../details/ui/resetcard/ResetCardFragment.kt | 2 +- .../details/ui/resetcard/ResetCardScreen.kt | 190 ++++++++++-------- .../{img_alert.xml => img_alert_80.xml} | 0 core/res/src/main/res/values-ru/strings.xml | 2 +- .../src/main/res/values-zh-rTW/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 2 +- .../com/tangem/core/ui/components/Dialogs.kt | 8 +- .../com/tangem/core/ui/res/TangemColors.kt | 19 +- .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../com/tangem/core/ui/res/TangemTheme.kt | 24 +-- .../tangem/core/ui/res/TangemTypography.kt | 2 +- .../ui/screen/ComposeBottomSheetFragment.kt | 2 +- .../feature/swap/ui/SwapScreenContent.kt | 2 +- .../common/component/TokenItem.kt | 32 +-- 15 files changed, 148 insertions(+), 143 deletions(-) rename app/src/main/res/drawable/{img_alert.xml => img_alert_80.xml} (100%) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt index e4198a521c..dfbc486b5c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt @@ -63,7 +63,8 @@ internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier checked = item.isChecked, enabled = item.isEnabled, onCheckedChange = item.onCheckedChange, - checkedColor = TangemTheme.colors.icon.accent, + checkedColor = TangemTheme.colors.control.checked, + uncheckedColor = TangemTheme.colors.icon.inactive, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt index c0ffda2914..5414ff05c6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt @@ -27,9 +27,9 @@ internal class ResetCardFragment : ComposeFragment(), StoreSubscriber Uni modifier = modifier, content = { when (state) { - is ResetCardScreenState.ResetCardScreenContent -> { - ResetCardView(state = state) - } + is ResetCardScreenState.ResetCardScreenContent -> ResetCardView(state = state) ResetCardScreenState.InitialState -> { // do nothing for now, just white screen } @@ -43,83 +37,86 @@ internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Uni ) } -@Suppress("LongMethod", "MagicNumber") @Composable private fun ResetCardView(state: ResetCardScreenState.ResetCardScreenContent) { + val scrollState = rememberScrollState() + Column( modifier = Modifier .fillMaxSize() - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.SpaceBetween, + .verticalScroll(scrollState) // set scrollState after fillMaxSize + .padding(horizontal = TangemTheme.dimens.spacing20), ) { - ScreenTitle(titleRes = R.string.card_settings_reset_card_to_factory) - Box( - modifier = Modifier - .weight(1f) - .padding(horizontal = 21.dp), - contentAlignment = Alignment.CenterStart, - ) { - Icon( - painter = painterResource(id = R.drawable.img_alert), - contentDescription = "", - tint = Color.Unspecified, - ) - } - Column( - modifier = Modifier.offset(y = (-32).dp), - verticalArrangement = Arrangement.Bottom, - ) { - Text( - text = stringResource(id = R.string.common_attention), - modifier = Modifier.padding(start = 20.dp, end = 20.dp), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) + Title() + SpacerH24() + AlertImage() + SpacerH24() + Subtitle() + SpacerH16() + Description(text = state.descriptionText) + SpacerH12() + Conditions(state) + DynamicSpacer(scrollState = scrollState) + SpacerH16() + ResetButton(enabled = state.resetButtonEnabled, onResetButtonClick = state.onResetButtonClick) + SpacerH16() + } +} - Spacer(modifier = Modifier.size(24.dp)) +@Composable +private fun Title() { + Text( + text = stringResource(id = R.string.card_settings_reset_card_to_factory), + style = TangemTheme.typography.h1, + color = TangemTheme.colors.text.primary1, + ) +} - Text( - text = state.descriptionText.resolveReference(), - modifier = Modifier.padding(start = 20.dp, end = 20.dp), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - ) +@Composable +private fun AlertImage() { + Image( + painter = painterResource(id = R.drawable.img_alert_80), + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size80), + ) +} - Spacer(modifier = Modifier.size(28.dp)) +@Composable +private fun Subtitle() { + Text( + text = stringResource(id = R.string.common_attention), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) +} - state.warningsToShow.forEach { - when (it) { - ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> { - ConditionCheckBox( - checkedState = state.acceptCondition1Checked, - onCheckedChange = state.onAcceptCondition1ToggleClick, - description = TextReference.Res(R.string.reset_card_to_factory_condition_1), - ) - } - ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> { - ConditionCheckBox( - checkedState = state.acceptCondition2Checked, - onCheckedChange = state.onAcceptCondition2ToggleClick, - description = TextReference.Res(R.string.reset_card_to_factory_condition_2), - ) - } - } +@Composable +private fun Description(text: TextReference) { + Text( + text = text.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + ) +} + +@Composable +private fun Conditions(state: ResetCardScreenState.ResetCardScreenContent) { + state.warningsToShow.forEach { + when (it) { + ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> { + ConditionCheckBox( + checkedState = state.acceptCondition1Checked, + onCheckedChange = state.onAcceptCondition1ToggleClick, + description = TextReference.Res(R.string.reset_card_to_factory_condition_1), + ) } - Spacer(modifier = Modifier.size(16.dp)) - Box( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 32.dp), - ) { - AnimatedContent( - targetState = state.resetButtonEnabled, - label = "Update checked state", - ) { buttonEnabled -> - DetailsMainButton( - title = stringResource(id = R.string.reset_card_to_factory_button_title), - onClick = state.onResetButtonClick, - enabled = buttonEnabled, - ) - } + ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> { + ConditionCheckBox( + checkedState = state.acceptCondition2Checked, + onCheckedChange = state.onAcceptCondition2ToggleClick, + description = TextReference.Res(R.string.reset_card_to_factory_condition_2), + ) } } } @@ -130,16 +127,11 @@ private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean) Row( modifier = Modifier .fillMaxWidth() - .clickable( - onClick = { onCheckedChange.invoke(!checkedState) }, - ) - .padding(top = TangemTheme.dimens.size16, bottom = TangemTheme.dimens.size16), + .clickable(onClick = { onCheckedChange.invoke(!checkedState) }) + .padding(vertical = TangemTheme.dimens.size16), + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), ) { - IconToggleButton( - checked = checkedState, - onCheckedChange = onCheckedChange, - modifier = Modifier.padding(start = TangemTheme.dimens.size20, end = TangemTheme.dimens.size20), - ) { + IconToggleButton(checked = checkedState, onCheckedChange = onCheckedChange) { AnimatedContent(targetState = checkedState, label = "Update checked state") { checked -> Icon( painter = painterResource( @@ -151,22 +143,44 @@ private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean) ), contentDescription = null, tint = if (checked) { - TangemTheme.colors.icon.accent + TangemTheme.colors.control.checked } else { TangemTheme.colors.icon.secondary }, ) } } + Text( text = description.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, - modifier = Modifier.padding(end = TangemTheme.dimens.size20), ) } } +/** + * It's helps to create an adaptive layout. + * ResetButton will be attach to the bottom of large screen or will be inside scroll layout of small screen. + * + * @param scrollState flag determines if screen is small (has scroll) or large (hasn't scroll) + */ +@Composable +private fun ColumnScope.DynamicSpacer(scrollState: ScrollState) { + if (!scrollState.canScrollBackward && !scrollState.canScrollForward) { + Spacer(modifier = Modifier.weight(1f)) + } +} + +@Composable +private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) { + DetailsMainButton( + title = stringResource(id = R.string.reset_card_to_factory_button_title), + onClick = onResetButtonClick, + enabled = enabled, + ) +} + // region Preview @Composable private fun ResetCardScreenSample(modifier: Modifier = Modifier) { diff --git a/app/src/main/res/drawable/img_alert.xml b/app/src/main/res/drawable/img_alert_80.xml similarity index 100% rename from app/src/main/res/drawable/img_alert.xml rename to app/src/main/res/drawable/img_alert_80.xml diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index d5cb28c7c0..aa633f341a 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -655,7 +655,7 @@ Dapp не предоставил необходимые данные для открытия сессии WalletConnect Не удалось найти сессию для обработки запроса Сессии WalletConnect - Подключение к Dapps + Подключение к dApps WalletConnect Транзакция успешно подписана и отправлена ​​в Dapp Транзакция успешно подписана и отправлена ​​в блокчейн 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 a1d2ee341a..6f802b6be1 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -425,7 +425,7 @@ Dapp 沒有提供必要的數據來建立 WalletConnect 連接 找不到請求的連接 WalletConnect 連接 - 連結到Dapps + 連結到dApps WalletConnect 交易已成功簽署並發送至 Dapp 交易已成功簽署並發送至區塊鏈 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 82870064e4..2a104482ff 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -671,7 +671,7 @@ Dapp didn\'t provide essential data to establish WalletConnect session Failed to find session for request WalletConnect Sessions - Connect to Dapps + Connect to dApps WalletConnect The transaction has been successfully signed and sent to the Dapp The transaction has been succesfully signed and sent to the blockchain diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index f332e61cbf..658d6af9f6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -7,7 +7,9 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.* +import androidx.compose.material.RadioButton +import androidx.compose.material.RadioButtonDefaults +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember @@ -210,7 +212,7 @@ private fun TangemDialog( modifier = Modifier .background( shape = TangemTheme.shapes.roundedCornersLarge, - color = TangemTheme.colors.background.plain, + color = TangemTheme.colors.background.primary, ) .padding(vertical = TangemTheme.dimens.spacing24), ) { @@ -386,7 +388,7 @@ private fun SelectorDialogContent( selected = index == selectedItemIndex, onClick = onClick, colors = RadioButtonDefaults.colors( - selectedColor = TangemTheme.colors.icon.accent, + selectedColor = TangemTheme.colors.control.checked, unselectedColor = TangemTheme.colors.icon.secondary, ), interactionSource = interactionSource, diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt index e814d46e20..451207be18 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt @@ -42,7 +42,7 @@ class TangemColors internal constructor( warning: Color, attention: Color, accent: Color = TangemColorPalette.Azure, - constantWhite: Color = TangemColorPalette.White, + constant: Color = TangemColorPalette.White, ) { var primary1 by mutableStateOf(primary1) private set @@ -60,7 +60,7 @@ class TangemColors internal constructor( private set var attention by mutableStateOf(attention) private set - var constantWhite by mutableStateOf(constantWhite) + var constantWhite by mutableStateOf(constant) private set fun update(other: Text) { @@ -83,6 +83,7 @@ class TangemColors internal constructor( warning: Color, attention: Color, accent: Color = TangemColorPalette.Azure, + constant: Color = TangemColorPalette.White, ) { var primary1 by mutableStateOf(primary1) private set @@ -118,8 +119,7 @@ class TangemColors internal constructor( primary: Color, secondary: Color, disabled: Color, - positive: Color = TangemColorPalette.Meadow, - positiveDisabled: Color, + positive: Color = TangemColorPalette.Azure, ) { var primary by mutableStateOf(primary) private set @@ -129,15 +129,12 @@ class TangemColors internal constructor( private set var positive by mutableStateOf(positive) private set - var positiveDisabled by mutableStateOf(positiveDisabled) - private set fun update(other: Button) { primary = other.primary secondary = other.secondary disabled = other.disabled positive = other.positive - positiveDisabled = other.positiveDisabled } } @@ -146,9 +143,7 @@ class TangemColors internal constructor( primary: Color, secondary: Color, tertiary: Color, - plain: Color, action: Color, - fade: Color, ) { var primary by mutableStateOf(primary) private set @@ -156,20 +151,14 @@ class TangemColors internal constructor( private set var tertiary by mutableStateOf(tertiary) private set - var plain by mutableStateOf(plain) - private set var action by mutableStateOf(action) private set - var fade by mutableStateOf(fade) - private set fun update(other: Background) { primary = other.primary secondary = other.secondary tertiary = other.tertiary - plain = other.plain action = other.action - fade = other.fade } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index e68dfd96a0..7be8761543 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -51,6 +51,7 @@ data class TangemDimens internal constructor( val size11: Dp = 11.dp, val size12: Dp = 12.dp, val size14: Dp = 14.dp, + val size15: Dp = 15.dp, val size16: Dp = 16.dp, val size18: Dp = 18.dp, val size20: Dp = 20.dp, diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index f87f80fe61..78d91538e0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -69,7 +69,7 @@ private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors { secondary = colors.button.primary, secondaryVariant = colors.text.accent, background = colors.background.primary, - surface = colors.background.plain, + surface = colors.background.secondary, error = colors.text.warning, onPrimary = colors.text.primary1, onSecondary = colors.text.primary1, @@ -94,10 +94,10 @@ private fun lightThemeColors(): TangemColors { attention = TangemColorPalette.Tangerine, ), icon = TangemColors.Icon( - primary1 = TangemColorPalette.Black, + primary1 = TangemColorPalette.Dark6, primary2 = TangemColorPalette.White, secondary = TangemColorPalette.Dark2, - informative = TangemColorPalette.Light5, + informative = TangemColorPalette.Dark1, inactive = TangemColorPalette.Light4, warning = TangemColorPalette.Amaranth, attention = TangemColorPalette.Tangerine, @@ -106,15 +106,12 @@ private fun lightThemeColors(): TangemColors { primary = TangemColorPalette.Dark6, secondary = TangemColorPalette.Light2, disabled = TangemColorPalette.Light2, - positiveDisabled = TangemColorPalette.MagicMint, ), background = TangemColors.Background( primary = TangemColorPalette.White, secondary = TangemColorPalette.Light1, tertiary = TangemColorPalette.Light1, - plain = TangemColorPalette.White, action = TangemColorPalette.White, - fade = TangemColorPalette.White, ), control = TangemColors.Control( checked = TangemColorPalette.Dark6, @@ -123,7 +120,7 @@ private fun lightThemeColors(): TangemColors { ), stroke = TangemColors.Stroke( primary = TangemColorPalette.Light2, - secondary = TangemColorPalette.Dark4, + secondary = TangemColorPalette.Dark5, transparency = TangemColorPalette.White, ), field = TangemColors.Field( @@ -149,25 +146,22 @@ private fun darkThemeColors(): TangemColors { icon = TangemColors.Icon( primary1 = TangemColorPalette.White, primary2 = TangemColorPalette.Dark6, - secondary = TangemColorPalette.Dark1, - informative = TangemColorPalette.Dark2, - inactive = TangemColorPalette.Dark4, + secondary = TangemColorPalette.Light5, + informative = TangemColorPalette.Dark1, + inactive = TangemColorPalette.Dark3, warning = TangemColorPalette.Flamingo, attention = TangemColorPalette.Mustard, ), button = TangemColors.Button( - primary = TangemColorPalette.Light4, + primary = TangemColorPalette.Light2, secondary = TangemColorPalette.Dark4, disabled = TangemColorPalette.Dark5, - positiveDisabled = TangemColorPalette.DarkGreen, ), background = TangemColors.Background( primary = TangemColorPalette.Dark6, secondary = TangemColorPalette.Black, tertiary = TangemColorPalette.Dark6, - plain = TangemColorPalette.Black, action = TangemColorPalette.Dark5, - fade = TangemColorPalette.Black, ), control = TangemColors.Control( checked = TangemColorPalette.Azure, @@ -176,7 +170,7 @@ private fun darkThemeColors(): TangemColors { ), stroke = TangemColors.Stroke( primary = TangemColorPalette.Dark4, - secondary = TangemColorPalette.Dark1, + secondary = TangemColorPalette.Dark4, transparency = TangemColorPalette.Dark6, ), field = TangemColors.Field( diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt index f2572e21ff..0187255098 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt @@ -78,7 +78,7 @@ data class TangemTypography internal constructor( fontSize = 14.sp, fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp), - lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp), + lineHeight = TextUnit(value = 16f, type = TextUnitType.Sp), ), val caption1: TextStyle = TextStyle( fontFamily = RobotoFamily, diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt index c361732038..b437a39cd5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt @@ -45,7 +45,7 @@ abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), Compose if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it } .background( - color = TangemTheme.colors.background.plain, + color = TangemTheme.colors.background.primary, shape = TangemTheme.shapes.bottomSheet, ) 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 8542161c27..f58b501863 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 @@ -280,7 +280,7 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { Card( elevation = TangemTheme.dimens.elevation3, shape = CircleShape, - backgroundColor = TangemTheme.colors.background.plain, + backgroundColor = TangemTheme.colors.background.action, contentColor = TangemTheme.colors.text.primary1, modifier = modifier.size(TangemTheme.dimens.size48), onClick = state.onChangeCardsClicked, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 9a3d17502f..724291e0bd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -7,7 +7,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.composed -import androidx.compose.ui.layout.* +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -123,8 +126,9 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier Layout(content = content, modifier = modifier) { measurables, constraints -> val layoutWidth = constraints.maxWidth - val layoutPadding = with(density) { dimens.size14.roundToPx() } - val layoutWidthWithoutPaddings = layoutWidth - 2 * layoutPadding + val horizontalPadding = with(density) { dimens.size14.roundToPx() } + val verticalPadding = with(density) { dimens.size15.roundToPx() } + val layoutWidthWithoutPaddings = layoutWidth - 2 * horizontalPadding val titleMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt() val priceChangeMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt() @@ -209,7 +213,7 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier val layoutHeight = calculateLayoutHeight( state = state, minLayoutHeight = with(density) { dimens.size68.roundToPx() }, - layoutPadding = layoutPadding, + layoutPadding = horizontalPadding, betweenRowsPadding = with(density) { dimens.size2.roundToPx() }, title = title, fiatAmount = fiatAmount, @@ -218,35 +222,35 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier ) layout(width = constraints.maxWidth, height = layoutHeight) { - icon.placeRelative(x = layoutPadding, y = (layoutHeight - icon.height).div(other = 2)) + icon.placeRelative(x = horizontalPadding, y = (layoutHeight - icon.height).div(other = 2)) title.placeRelative( - x = layoutPadding + icon.width, + x = horizontalPadding + icon.width, y = when (state) { is TokenItemState.NoAddress, is TokenItemState.Unreachable, -> (layoutHeight - title.height).div(other = 2) - else -> layoutPadding + else -> verticalPadding }, ) - fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width - layoutPadding, y = layoutPadding) + fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width - horizontalPadding, y = verticalPadding) priceChange?.placeRelative( - x = layoutPadding + icon.width, - y = layoutHeight - priceChange.height - layoutPadding, + x = horizontalPadding + icon.width, + y = layoutHeight - priceChange.height - verticalPadding, ) cryptoAmount?.placeRelative( x = when (state) { - is TokenItemState.Draggable -> layoutPadding + icon.width - else -> layoutWidth - cryptoAmount.width - layoutPadding + is TokenItemState.Draggable -> horizontalPadding + icon.width + else -> layoutWidth - cryptoAmount.width - horizontalPadding }, - y = layoutHeight - cryptoAmount.height - layoutPadding, + y = layoutHeight - cryptoAmount.height - verticalPadding, ) nonFiatContent.placeRelative( - x = layoutWidth - nonFiatContent.width - layoutPadding, + x = layoutWidth - nonFiatContent.width - horizontalPadding, y = (layoutHeight - nonFiatContent.height).div(other = 2), ) } From 21f0cfaf1f9057df977fd38d56eebde93cb89e28 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jan 2024 15:11:55 +0300 Subject: [PATCH 20/28] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 5 +- core/res/src/main/res/values/strings.xml | 9 +- .../presentation/state/SendStateFactory.kt | 7 +- .../impl/presentation/state/SendUiState.kt | 3 + .../state/fee/FeeNotificationFactory.kt | 41 ++++++--- .../state/fee/SendFeeNotification.kt | 28 ++++-- .../state/fee/SendFeeStateConverter.kt | 11 ++- .../ui/fee/SendSpeedAndFeeContent.kt | 2 +- .../presentation/ui/fee/SendSpeedSelector.kt | 88 +++++++++++++------ .../impl/presentation/ui/send/SendContent.kt | 20 +++-- 10 files changed, 147 insertions(+), 67 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index aa633f341a..460c4ae43b 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -70,7 +70,7 @@ биометрическую аутентификацию биометрией Купить - Купить %1$s + Перейти на %1$s Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности. Отмена Закрыть @@ -478,8 +478,6 @@ Покрытие сетевой комиссии Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса Недостаточно средств - Размер комиссии превышает баланс сети. Для продолжения необходимо пополнить баланс сети. - Комиссия превышает баланс Увеличение комиссии Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. Оставить %s XTZ @@ -494,7 +492,6 @@ Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции Возможны задержки по транзакции Необязательное - QR код содержит информацию о сумме отправки равной %s Последние Получатель Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 2a104482ff..75ab3f3f04 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -68,7 +68,7 @@ biometric authentication biometrics Buy - Buy %1$s + Go to %1$s You have not given access to your camera, please adjust your privacy settings Cancel Close @@ -483,8 +483,6 @@ Network fee coverage Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance Total exceeds balance - The commission fee exceeds the network balance. To continue, it is necessary to replenish the network balance. - Fee exceeds balance 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 @@ -503,11 +501,6 @@ Existential deposit The account will be wiped from the blockchain if a balance goes below the existential deposit. Please ensure that the remaining balance after sending will not be less than %s. Optional - Change - Decline - Recipient’s address scanned - Change the entered amount? - QR code contains information about the sending amount equal to %s Please align your QR code with the square to scan it. Ensure you scan %s network address. Recent Recipient 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 23b9ce0dfb..17ca733eee 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 @@ -75,7 +75,12 @@ internal class SendStateFactory( cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } - private val feeStateConverter by lazy { SendFeeStateConverter() } + private val feeStateConverter by lazy { + SendFeeStateConverter( + appCurrencyProvider = appCurrencyProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } private val recipientListStateConverter by lazy { SendRecipientListConverter( 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 4407d17d0f..14b9295f17 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 @@ -7,6 +7,7 @@ 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.core.ui.extensions.TextReference +import com.tangem.domain.appcurrency.model.AppCurrency 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 @@ -74,6 +75,8 @@ internal sealed class SendStates { val fee: Fee?, val receivedAmountValue: BigDecimal, val receivedAmount: String, + val rate: BigDecimal?, + val appCurrency: AppCurrency, val notifications: ImmutableList, ) : SendStates() 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 c434a6182f..822c7f43af 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 @@ -7,6 +7,7 @@ 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.R 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 @@ -118,25 +119,37 @@ internal class FeeNotificationFactory( add( SendFeeNotification.Error.ExceedsBalance( warning.coinCurrency.networkIconResId, - ) { - clickIntents.onTokenDetailsClick( - userWalletProvider().walletId, - warning.coinCurrency, - ) - }, + networkName = warning.coinCurrency.name, + currencyName = cryptoCurrencyStatus.currency.name, + feeName = warning.coinCurrency.name, + feeSymbol = warning.coinCurrency.symbol, + onClick = { + clickIntents.onTokenDetailsClick( + userWalletId = userWalletId, + currency = warning.coinCurrency, + ) + }, + ), ) } is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> { - val currency = warning.feeCurrency ?: warning.currency + val currency = warning.feeCurrency add( SendFeeNotification.Error.ExceedsBalance( - currency.networkIconResId, - ) { - clickIntents.onTokenDetailsClick( - userWalletId, - currency, - ) - }, + networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24, + currencyName = warning.currency.name, + feeName = warning.feeCurrencyName, + feeSymbol = warning.feeCurrencySymbol, + networkName = warning.networkName, + onClick = currency?.let { + { + clickIntents.onTokenDetailsClick( + userWalletId, + currency, + ) + } + }, + ), ) } else -> Unit 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 fbf72931af..d58cb3cd98 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 @@ -62,7 +62,7 @@ sealed class SendFeeNotification(val config: NotificationConfig) { val title: TextReference, val subtitle: TextReference, val iconResId: Int, - val buttonsState: NotificationConfig.ButtonsState, + val buttonsState: NotificationConfig.ButtonsState? = null, ) : SendFeeNotification( config = NotificationConfig( title = title, @@ -73,15 +73,27 @@ sealed class SendFeeNotification(val config: NotificationConfig) { ) { data class ExceedsBalance( val networkIconId: Int, - val onClick: () -> Unit, + val currencyName: String, + val feeName: String, + val feeSymbol: String, + val networkName: String, + val onClick: (() -> Unit)? = null, ) : Error( - title = resourceReference(id = R.string.send_notification_exceed_fee_title), - subtitle = resourceReference(id = R.string.send_notification_exceed_fee_text), - iconResId = networkIconId, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.common_go_to_provider), - onClick = onClick, + title = resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_title, + wrappedList(feeName), ), + subtitle = resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_message, + formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol), + ), + iconResId = networkIconId, + buttonsState = onClick?.let { + NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.common_buy_currency, wrappedList(feeName)), + onClick = onClick, + ) + }, ) } } \ 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 312ed32c90..2231fab584 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,13 +1,20 @@ package com.tangem.features.send.impl.presentation.state.fee +import com.tangem.domain.appcurrency.model.AppCurrency +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 : Converter { +internal class SendFeeStateConverter( + private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { override fun convert(value: Unit): SendStates.FeeState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() return SendStates.FeeState( feeSelectorState = FeeSelectorState.Loading, isSubtractAvailable = false, @@ -17,6 +24,8 @@ internal class SendFeeStateConverter : Converter { receivedAmountValue = BigDecimal.ZERO, receivedAmount = "", notifications = persistentListOf(), + rate = cryptoCurrencyStatus.value.fiatRate, + appCurrency = appCurrencyProvider(), ) } } \ No newline at end of file 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 43ab51b3ac..b253efe2e5 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 @@ -40,7 +40,7 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S key = FEE_SELECTOR_KEY, ) { SendSpeedSelector( - state = feeSendState, + state = state, clickIntents = clickIntents, modifier = Modifier.animateItemPlacement(), ) 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 db6be28780..59e6fd4cc2 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 @@ -19,21 +19,27 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme 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.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.ui.common.FooterContainer import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import java.math.BigDecimal private val DEFAULT_FEE_OPTIONS = listOf( R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24, @@ -44,7 +50,7 @@ private val DEFAULT_FEE_OPTIONS = listOf( @Suppress("LongMethod") @Composable internal fun SendSpeedSelector( - state: FeeSelectorState, + state: SendStates.FeeState, clickIntents: SendClickIntents, modifier: Modifier = Modifier, ) { @@ -58,7 +64,7 @@ internal fun SendSpeedSelector( .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action), ) { - when (state) { + when (val feeSelectorState = state.feeSelectorState) { FeeSelectorState.Error -> { SendSpeedSelectorItemError() } @@ -66,36 +72,42 @@ internal fun SendSpeedSelector( SendSpeedSelectorItemLoading() } is FeeSelectorState.Content -> { - when (state.fees) { + when (val fees = feeSelectorState.fees) { is TransactionFee.Choosable -> { - val isSelected = state.selectedFee + val isSelected = feeSelectorState.selectedFee + val minimumAmount = fees.minimum.amount SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_slow, iconRes = R.drawable.ic_tortoise_24, - amount = TextReference.Str(state.fees.minimum.amount.value.toString()), - symbol = TextReference.Str(state.fees.minimum.amount.currencySymbol), + amount = getCryptoReference(minimumAmount), + fiatAmount = getFiatReference(minimumAmount, state.rate, state.appCurrency), + symbolLength = minimumAmount.currencySymbol.length, isSelected = isSelected == FeeType.SLOW, onSelect = { clickIntents.onFeeSelectorClick(FeeType.SLOW) }, ) + val normalAmount = fees.normal.amount SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_market, iconRes = R.drawable.ic_bird_24, - amount = TextReference.Str(state.fees.normal.amount.value.toString()), - symbol = TextReference.Str(state.fees.normal.amount.currencySymbol), + amount = getCryptoReference(normalAmount), + fiatAmount = getFiatReference(normalAmount, state.rate, state.appCurrency), + symbolLength = normalAmount.currencySymbol.length, isSelected = isSelected == FeeType.MARKET, onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) }, ) + val priorityAmount = fees.priority.amount SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_fast, iconRes = R.drawable.ic_hare_24, - amount = TextReference.Str(state.fees.priority.amount.value.toString()), - symbol = TextReference.Str(state.fees.priority.amount.currencySymbol), + amount = getCryptoReference(priorityAmount), + fiatAmount = getFiatReference(priorityAmount, state.rate, state.appCurrency), + symbolLength = priorityAmount.currencySymbol.length, isSelected = isSelected == FeeType.FAST, onSelect = { clickIntents.onFeeSelectorClick(FeeType.FAST) }, - showDivider = state.fees.normal is Fee.Ethereum, + showDivider = fees.normal is Fee.Ethereum, ) AnimatedVisibility( - visible = state.fees.normal is Fee.Ethereum, + visible = fees.normal is Fee.Ethereum, label = "Custom fee appearance animation", ) { SendSpeedSelectorItem( @@ -103,17 +115,19 @@ internal fun SendSpeedSelector( iconRes = R.drawable.ic_edit_24, isSelected = isSelected == FeeType.CUSTOM, onSelect = { clickIntents.onFeeSelectorClick(FeeType.CUSTOM) }, - showDivider = state.fees.normal !is Fee.Ethereum, + showDivider = fees.normal !is Fee.Ethereum, ) } } is TransactionFee.Single -> { + val normalAmount = feeSelectorState.fees.normal.amount SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_market, iconRes = R.drawable.ic_bird_24, isSelected = true, - amount = TextReference.Str(state.fees.normal.amount.value.toString()), - symbol = TextReference.Str(state.fees.normal.amount.currencySymbol), + amount = getCryptoReference(normalAmount), + fiatAmount = getFiatReference(normalAmount, state.rate, state.appCurrency), + symbolLength = normalAmount.currencySymbol.length, onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) }, showDivider = false, ) @@ -125,6 +139,24 @@ internal fun SendSpeedSelector( } } +// todo remove after refactoring [REDACTED_JIRA] +private fun getCryptoReference(amount: Amount) = stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = amount.value, + cryptoCurrency = amount.currencySymbol, + decimals = amount.decimals, + ), +) + +// todo remove after refactoring [REDACTED_JIRA] +private fun getFiatReference(amount: Amount, rate: BigDecimal?, appCurrency: AppCurrency) = stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = rate?.let { amount.value?.multiply(it) }, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), +) + @Composable private fun SendSpeedSelectorItemLoading() { repeat(DEFAULT_FEE_OPTIONS.size) { @@ -183,7 +215,8 @@ private fun SendSpeedSelectorItem( onSelect: () -> Unit, modifier: Modifier = Modifier, amount: TextReference? = null, - symbol: TextReference? = null, + fiatAmount: TextReference? = null, + symbolLength: Int? = null, isSelected: Boolean = false, showDivider: Boolean = true, ) { @@ -214,10 +247,11 @@ private fun SendSpeedSelectorItem( iconTint = iconTint, textStyle = textStyle, ) - if (amount != null && symbol != null) { + if (amount != null && symbolLength != null && fiatAmount != null) { SelectorValueContent( amount = amount, - symbol = symbol, + fiatAmount = fiatAmount, + symbolLength = symbolLength, textStyle = textStyle, ) } @@ -267,14 +301,18 @@ private fun SelectorTitleContent( } @Composable -private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextReference, textStyle: TextStyle) { - Text( +private fun RowScope.SelectorValueContent( + amount: TextReference, + fiatAmount: TextReference, + symbolLength: Int, + textStyle: TextStyle, +) { + EllipsisText( text = amount.resolveReference(), style = textStyle, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.End, - overflow = TextOverflow.Ellipsis, - maxLines = 1, + ellipsis = TextEllipsis.OffsetEnd(symbolLength), modifier = Modifier .weight(1f) .padding( @@ -284,12 +322,12 @@ private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextRef ), ) Text( - text = symbol.resolveReference(), + text = "(${fiatAmount.resolveReference()})", style = textStyle, color = TangemTheme.colors.text.primary1, modifier = Modifier .padding( - start = TangemTheme.dimens.spacing1, + start = TangemTheme.dimens.spacing4, end = TangemTheme.dimens.spacing12, top = TangemTheme.dimens.spacing14, bottom = TangemTheme.dimens.spacing14, 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 62330d3e7c..d014f71caa 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 @@ -24,9 +24,11 @@ import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount +import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount import com.tangem.domain.tokens.model.AmountType import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendNotification @@ -127,7 +129,7 @@ private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: Boolean, cryptoCurrency = amount.cryptoAmount.currencySymbol, decimals = amount.cryptoAmount.decimals, ) - val fiatAmount = BigDecimalFormatter.formatFiatAmount( + val fiatAmount = formatFiatAmount( fiatAmount = amount.fiatAmount.value, fiatCurrencyCode = (amount.fiatAmount.type as AmountType.FiatType).code, fiatCurrencySymbol = amount.fiatAmount.currencySymbol, @@ -174,14 +176,22 @@ private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess: @Composable private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) { val fee = feeState.fee ?: return - val feeValue = formatCryptoAmount( + val feeCryptoValue = formatCryptoAmount( cryptoAmount = fee.amount.value, cryptoCurrency = fee.amount.currencySymbol, decimals = fee.amount.decimals, ) + val feeFiatValue = formatFiatAmount( + fiatAmount = feeState.rate?.let { fee.amount.value?.multiply(it) }, + fiatCurrencyCode = feeState.appCurrency.code, + fiatCurrencySymbol = feeState.appCurrency.symbol, + ) InputRowDefault( - title = TextReference.Res(R.string.common_network_fee_title), - text = TextReference.Str(feeValue), + title = resourceReference(R.string.common_network_fee_title), + text = resourceReference( + id = R.string.send_wallet_balance_format, + wrappedList(feeCryptoValue, feeFiatValue), + ), modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) From 34393ef5e685f80f5be74a8a75f90193f9a63b75 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jan 2024 15:56:39 +0300 Subject: [PATCH 21/28] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 8 +++++++ .../core/ui/utils/BigDecimalFormatter.kt | 1 + .../data/transaction/DefaultFeeRepository.kt | 13 +++++++++++ .../transaction/di/TransactionDataModule.kt | 8 +++++++ .../domain/transaction/FeeRepository.kt | 10 ++++++++ .../usecase/IsFeeApproximateUseCase.kt | 19 +++++++++++++++ .../impl/presentation/state/SendUiState.kt | 1 + .../presentation/state/fee/FeeStateFactory.kt | 11 +++++++++ .../state/fee/SendFeeStateConverter.kt | 1 + .../presentation/ui/fee/SendSpeedSelector.kt | 23 +++++++++++-------- .../presentation/viewmodel/SendViewModel.kt | 3 +++ 11 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 data/transaction/src/main/java/com/tangem/data/transaction/DefaultFeeRepository.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/FeeRepository.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/IsFeeApproximateUseCase.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 456bf93702..5fc760156f 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 @@ -2,9 +2,11 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module @@ -42,4 +44,10 @@ internal object TransactionDomainModule { fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase { return CreateTransactionUseCase(transactionRepository) } + + @Provides + @ViewModelScoped + fun provideIsFeeApproximateUseCase(feeRepository: FeeRepository): IsFeeApproximateUseCase { + return IsFeeApproximateUseCase(feeRepository) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index e2d0c8dc42..c5fc3d7442 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -10,6 +10,7 @@ import java.util.Locale object BigDecimalFormatter { const val EMPTY_BALANCE_SIGN = "—" + const val CAN_BE_LOWER_SIGN = "<" private const val TEMP_CURRENCY_CODE = "USD" diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultFeeRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultFeeRepository.kt new file mode 100644 index 0000000000..25ee54c0b0 --- /dev/null +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultFeeRepository.kt @@ -0,0 +1,13 @@ +package com.tangem.data.transaction + +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.FeeRepository + +internal class DefaultFeeRepository : FeeRepository { + override fun isFeeApproximate(networkId: Network.ID, amountType: AmountType): Boolean { + val blockchain = Blockchain.fromId(networkId.value) + return blockchain.isFeeApproximate(amountType) + } +} \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt index de935654bb..c8949d39c0 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt @@ -1,6 +1,8 @@ package com.tangem.data.transaction.di +import com.tangem.data.transaction.DefaultFeeRepository import com.tangem.data.transaction.DefaultTransactionRepository +import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -25,4 +27,10 @@ internal object TransactionDataModule { coroutineDispatcherProvider = coroutineDispatcherProvider, ) } + + @Provides + @Singleton + fun providesFeeRepository(): FeeRepository { + return DefaultFeeRepository() + } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/FeeRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/FeeRepository.kt new file mode 100644 index 0000000000..9434d8cf80 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/FeeRepository.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.transaction + +import com.tangem.blockchain.common.AmountType +import com.tangem.domain.tokens.model.Network + +interface FeeRepository { + + /** Returns if fee is approximate for current [networkId] */ + fun isFeeApproximate(networkId: Network.ID, amountType: AmountType): Boolean +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/IsFeeApproximateUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/IsFeeApproximateUseCase.kt new file mode 100644 index 0000000000..688716716f --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/IsFeeApproximateUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.transaction.usecase + +import com.tangem.blockchain.common.AmountType +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.FeeRepository + +/** + * Use case to check if fee is approximate + * + * @param feeRepository [FeeRepository] + */ +class IsFeeApproximateUseCase( + private val feeRepository: FeeRepository, +) { + + operator fun invoke(networkId: Network.ID, amountType: AmountType): Boolean { + return feeRepository.isFeeApproximate(networkId, amountType) + } +} \ 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 14b9295f17..0e13e65c4f 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 @@ -77,6 +77,7 @@ internal sealed class SendStates { val receivedAmount: String, val rate: BigDecimal?, val appCurrency: AppCurrency, + val isFeeApproximate: Boolean, val notifications: ImmutableList, ) : SendStates() 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 dd003ef56f..c03cfaef65 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,6 +5,7 @@ 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.transaction.usecase.IsFeeApproximateUseCase 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 @@ -24,6 +25,7 @@ internal class FeeStateFactory( private val coinCryptoCurrencyStatusProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, private val appCurrencyProvider: Provider, + private val isFeeApproximateUseCase: IsFeeApproximateUseCase, ) { private val customFeeFieldConverter by lazy { SendFeeCustomFieldConverter( @@ -66,6 +68,7 @@ internal class FeeStateFactory( receivedAmountValue = receivedAmount, receivedAmount = getFormattedValue(receivedAmount), isSubtract = isSubtractAvailable && checkAutoSubtract(state, fee, balance), + isFeeApproximate = isFeeApproximate(fee), ), ) } @@ -206,6 +209,14 @@ internal class FeeStateFactory( } } + private fun isFeeApproximate(fee: Fee): Boolean { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + return isFeeApproximateUseCase( + networkId = cryptoCurrencyStatus.currency.network.id, + amountType = fee.amount.type, + ) + } + private fun getFormattedValue(value: BigDecimal): String { val cryptoCurrency = cryptoCurrencyStatusProvider().currency return BigDecimalFormatter.formatCryptoAmount( 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 2231fab584..1dbee6b3fd 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 @@ -26,6 +26,7 @@ internal class SendFeeStateConverter( notifications = persistentListOf(), rate = cryptoCurrencyStatus.value.fiatRate, appCurrency = appCurrencyProvider(), + isFeeApproximate = false, ) } } \ 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 59e6fd4cc2..db73ae5c7e 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 @@ -28,10 +28,12 @@ import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.BigDecimalFormatter.CAN_BE_LOWER_SIGN import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendStates @@ -79,7 +81,7 @@ internal fun SendSpeedSelector( SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_slow, iconRes = R.drawable.ic_tortoise_24, - amount = getCryptoReference(minimumAmount), + amount = getCryptoReference(minimumAmount, state.isFeeApproximate), fiatAmount = getFiatReference(minimumAmount, state.rate, state.appCurrency), symbolLength = minimumAmount.currencySymbol.length, isSelected = isSelected == FeeType.SLOW, @@ -89,7 +91,7 @@ internal fun SendSpeedSelector( SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_market, iconRes = R.drawable.ic_bird_24, - amount = getCryptoReference(normalAmount), + amount = getCryptoReference(normalAmount, state.isFeeApproximate), fiatAmount = getFiatReference(normalAmount, state.rate, state.appCurrency), symbolLength = normalAmount.currencySymbol.length, isSelected = isSelected == FeeType.MARKET, @@ -99,7 +101,7 @@ internal fun SendSpeedSelector( SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_fast, iconRes = R.drawable.ic_hare_24, - amount = getCryptoReference(priorityAmount), + amount = getCryptoReference(priorityAmount, state.isFeeApproximate), fiatAmount = getFiatReference(priorityAmount, state.rate, state.appCurrency), symbolLength = priorityAmount.currencySymbol.length, isSelected = isSelected == FeeType.FAST, @@ -125,7 +127,7 @@ internal fun SendSpeedSelector( titleRes = R.string.common_fee_selector_option_market, iconRes = R.drawable.ic_bird_24, isSelected = true, - amount = getCryptoReference(normalAmount), + amount = getCryptoReference(normalAmount, state.isFeeApproximate), fiatAmount = getFiatReference(normalAmount, state.rate, state.appCurrency), symbolLength = normalAmount.currencySymbol.length, onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) }, @@ -140,11 +142,14 @@ internal fun SendSpeedSelector( } // todo remove after refactoring [REDACTED_JIRA] -private fun getCryptoReference(amount: Amount) = stringReference( - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = amount.value, - cryptoCurrency = amount.currencySymbol, - decimals = amount.decimals, +private fun getCryptoReference(amount: Amount, isFeeApproximate: Boolean) = combinedReference( + if (isFeeApproximate) stringReference("$CAN_BE_LOWER_SIGN ") else TextReference.EMPTY, + stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = amount.value, + cryptoCurrency = amount.currencySymbol, + decimals = amount.decimals, + ), ), ) 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 32629e642a..80f5e20cac 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 @@ -24,6 +24,7 @@ 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.IsFeeApproximateUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase @@ -70,6 +71,7 @@ internal class SendViewModel @Inject constructor( private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, + private val isFeeApproximateUseCase: IsFeeApproximateUseCase, getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, validateWalletMemoUseCase: ValidateWalletMemoUseCase, getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, @@ -104,6 +106,7 @@ internal class SendViewModel @Inject constructor( coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + isFeeApproximateUseCase = isFeeApproximateUseCase, ) private val eventStateFactory = SendEventStateFactory( From 1d3f6ef13e1ff9ad1736b94a24efb484118966bf Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Feb 2024 17:04:15 +0300 Subject: [PATCH 22/28] Updated on 2026-08-14 --- .../state/SendEventStateFactory.kt | 3 +- .../impl/presentation/state/SendUiState.kt | 4 + .../presentation/state/fee/FeeCalculation.kt | 26 --- .../presentation/state/fee/FeeConverter.kt | 60 +++++++ .../state/fee/FeeNotificationFactory.kt | 11 +- .../presentation/state/fee/FeeStateFactory.kt | 39 +++-- .../state/fee/SendFeeCustomFieldConverter.kt | 86 +++------- .../fee/custom/EthereumCustomFeeConverter.kt | 159 ++++++++++++++++++ .../presentation/viewmodel/SendViewModel.kt | 10 +- gradle/dependencies.toml | 2 +- 10 files changed, 291 insertions(+), 109 deletions(-) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt 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 5ecb7dc55c..a175478e41 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 @@ -7,7 +7,6 @@ import com.tangem.domain.transaction.error.SendTransactionError 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 import java.math.BigDecimal @@ -58,7 +57,7 @@ internal class SendEventStateFactory( } val newFeeValue = newFee.amount.value ?: BigDecimal.ZERO - val oldFeeValue = feeSelector.getFee().amount.value ?: BigDecimal.ZERO + val oldFeeValue = feeStateFactory.feeConverter.convert(feeSelector).amount.value ?: BigDecimal.ZERO val updateFeeState = feeStateFactory.onFeeOnLoadedState(fee) return if (newFeeValue > oldFeeValue) { updateFeeState.copy( 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 0e13e65c4f..16d0b6190a 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 @@ -43,6 +43,7 @@ internal sealed class SendStates { abstract val isPrimaryButtonEnabled: Boolean /** Amount state */ + @Stable data class AmountState( override val type: SendUiStateType = SendUiStateType.Amount, override val isPrimaryButtonEnabled: Boolean, @@ -54,6 +55,7 @@ internal sealed class SendStates { ) : SendStates() /** Recipient state */ + @Stable data class RecipientState( override val type: SendUiStateType = SendUiStateType.Recipient, override val isPrimaryButtonEnabled: Boolean, @@ -65,6 +67,7 @@ internal sealed class SendStates { ) : SendStates() /** Fee and speed state */ + @Stable data class FeeState( override val type: SendUiStateType = SendUiStateType.Fee, override val isPrimaryButtonEnabled: Boolean = false, @@ -82,6 +85,7 @@ internal sealed class SendStates { ) : SendStates() /** Send state */ + @Stable data class SendState( override val type: SendUiStateType = SendUiStateType.Send, override val isPrimaryButtonEnabled: Boolean = true, 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 cd0877210f..16d58d39a6 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 @@ -1,8 +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.blockchain.extensions.toBigDecimalOrDefault import com.tangem.features.send.impl.presentation.state.SendUiState import java.math.BigDecimal @@ -13,28 +11,4 @@ internal fun calculateReceiveAmount(state: SendUiState, feeAmount: Fee): BigDeci val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO val fee = feeAmount.amount.value ?: return BigDecimal.ZERO return amountValue.minus(fee) -} - -/** - * Returns fee amount depending on current state - */ -internal fun FeeSelectorState.Content.getFee(): Fee { - return when (fees) { - is TransactionFee.Choosable -> { - when (selectedFee) { - FeeType.SLOW -> fees.minimum - FeeType.MARKET -> fees.normal - FeeType.FAST -> fees.priority - FeeType.CUSTOM -> { - val feeAmount = customValues.firstOrNull()?.value.toBigDecimalOrDefault() - Fee.Common( - fees.normal.amount.copy( - value = feeAmount, - ), - ) - } - } - } - is TransactionFee.Single -> fees.normal - } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt new file mode 100644 index 0000000000..c14db17575 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt @@ -0,0 +1,60 @@ +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.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter + +internal class FeeConverter( + private val clickIntents: SendClickIntents, + private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + + private val ethereumCustomFeeConverter by lazy { + EthereumCustomFeeConverter( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + + override fun convert(value: FeeSelectorState.Content): Fee { + return when (val fees = value.fees) { + is TransactionFee.Choosable -> { + when (value.selectedFee) { + FeeType.SLOW -> fees.minimum + FeeType.MARKET -> fees.normal + FeeType.FAST -> fees.priority + FeeType.CUSTOM -> convertCustom(value, fees) + } + } + is TransactionFee.Single -> fees.normal + } + } + + private fun convertCustom(feeSelectorState: FeeSelectorState.Content, fees: TransactionFee): Fee { + val customValues = feeSelectorState.customValues + val normalFee = fees.normal + return if (customValues.isEmpty()) { + normalFee + } else { + when (normalFee) { + is Fee.Ethereum -> ethereumCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues) + else -> { + val customFee = customValues.firstOrNull() + Fee.Common( + normalFee.amount.copy( + value = customFee?.value?.parseToBigDecimal(customFee.decimals), + ), + ) + } + } + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt index 822c7f43af..244ed17cdd 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,6 +3,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.core.ui.extensions.networkIconResId +import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -14,6 +15,7 @@ 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 com.tangem.utils.toFormattedString import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.filter @@ -65,7 +67,8 @@ internal class FeeNotificationFactory( ) { val multipleFees = transactionFee as? TransactionFee.Choosable ?: return val minimumValue = multipleFees.minimum.amount.value ?: return - val customValue = customFee.firstOrNull()?.value?.toBigDecimalOrNull() ?: return + val customAmount = customFee.firstOrNull() ?: return + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) if (selectedFee == FeeType.CUSTOM && minimumValue > customValue) { add(SendFeeNotification.Informational.TooLow) } @@ -78,10 +81,11 @@ internal class FeeNotificationFactory( ) { val multipleFees = transactionFee as? TransactionFee.Choosable ?: return val highValue = multipleFees.priority.amount.value ?: return - val customValue = customFee.firstOrNull()?.value?.toBigDecimalOrNull() ?: return + val customAmount = customFee.firstOrNull() ?: return + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) val diff = customValue / highValue if (selectedFee == FeeType.CUSTOM && diff > FEE_MAX_DIFF) { - add(SendFeeNotification.Warning.TooHigh(diff.toInt().toString())) + add(SendFeeNotification.Warning.TooHigh(diff.toFormattedString(HIGH_FEE_DIFF_DECIMALS))) } } @@ -158,5 +162,6 @@ internal class FeeNotificationFactory( companion object { private val FEE_MAX_DIFF = BigDecimal(5) + private const val HIGH_FEE_DIFF_DECIMALS = 0 } } \ No newline at end of file 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 c03cfaef65..fcbfd66705 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 @@ -2,7 +2,9 @@ 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.common.extensions.isZero import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase @@ -10,10 +12,8 @@ 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 /** @@ -31,6 +31,15 @@ internal class FeeStateFactory( SendFeeCustomFieldConverter( clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + + val feeConverter by lazy { + FeeConverter( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } @@ -58,7 +67,7 @@ internal class FeeStateFactory( customValues = customFeeFieldConverter.convert(fees.normal), ) - val fee = feeSelectorState.getFee() + val fee = feeConverter.convert(feeSelectorState) val receivedAmount = calculateReceiveAmount(state, fee) return state.copy( feeState = feeState.copy( @@ -83,7 +92,7 @@ internal class FeeStateFactory( fees = fees, customValues = customFeeFieldConverter.convert(fees.normal), ) - val fee = updatedFeeSelector.getFee() + val fee = feeConverter.convert(updatedFeeSelector) val receivedAmount = calculateReceiveAmount(state, fee) return state.copy( feeState = feeState.copy( @@ -112,7 +121,7 @@ internal class FeeStateFactory( val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType) - val fee = updatedFeeSelectorState.getFee() + val fee = feeConverter.convert(updatedFeeSelectorState) val receivedAmount = calculateReceiveAmount(state, fee) return state.copy( feeState = feeState.copy( @@ -129,14 +138,10 @@ internal class FeeStateFactory( 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 updatedFeeSelectorState = customFeeFieldConverter.onValueChange(feeSelectorState, index, value) val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO - val fee = updatedFeeSelectorState.getFee() + val fee = feeConverter.convert(updatedFeeSelectorState) val receivedAmount = calculateReceiveAmount(state, fee) return state.copy( feeState = feeState.copy( @@ -153,7 +158,7 @@ internal class FeeStateFactory( val state = currentStateProvider() val feeState = state.feeState ?: return state val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state - val fee = feeSelectorState.getFee() + val fee = feeConverter.convert(feeSelectorState) val receivedAmount = calculateReceiveAmount(state, fee) return state.copy( feeState = feeState.copy( @@ -181,13 +186,17 @@ internal class FeeStateFactory( notifications: ImmutableList, ): Boolean { val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return false - val customValue = feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull() + val customValue = feeSelectorState.customValues.firstOrNull() val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO - val fee = feeSelectorState.getFee() + val fee = feeConverter.convert(feeSelectorState) val feeValue = fee.amount.value ?: BigDecimal.ZERO val isNotCustom = feeSelectorState.selectedFee != FeeType.CUSTOM - val isNotEmptyCustom = !customValue.isNullOrZero() && !isNotCustom + val isNotEmptyCustom = if (customValue != null) { + !customValue.value.parseToBigDecimal(customValue.decimals).isZero() && !isNotCustom + } else { + false + } val noErrors = notifications.none { it is SendFeeNotification.Error } val isSubtractRequired = when { !feeState.isSubtractAvailable -> true // current currency is not fee currency 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 d6d6168f37..24c85ee305 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 @@ -1,79 +1,47 @@ package com.tangem.features.send.impl.presentation.state.fee -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.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.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter 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 internal class SendFeeCustomFieldConverter( private val clickIntents: SendClickIntents, private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, ) : Converter> { + private val ethereumCustomFeeConverter by lazy { + EthereumCustomFeeConverter( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + override fun convert(value: Fee): ImmutableList { - val ethereumFee = value as? Fee.Ethereum ?: return persistentListOf() - val appCurrency = appCurrencyProvider() - - val maxFeeFiat = BigDecimalFormatter.formatFiatAmount( - fiatAmount = ethereumFee.amount.value, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - - return persistentListOf( - SendTextField.CustomFee( - 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, - ), - 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, - keyboardType = KeyboardType.Number, - ), - ), - 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, - keyboardType = KeyboardType.Number, - ), - ), - ) + return when (value) { + is Fee.Ethereum -> { + ethereumCustomFeeConverter.convert(value) + } + else -> persistentListOf() + } } - companion object { - private const val ETHEREUM_UNIT = "GWEI" - } + fun onValueChange(feeSelectorState: FeeSelectorState.Content, index: Int, value: String) = feeSelectorState.copy( + customValues = when (feeSelectorState.fees.normal) { + is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange( + customValues = feeSelectorState.customValues, + index = index, + value = value, + ) + else -> feeSelectorState.customValues + }, + ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt new file mode 100644 index 0000000000..4997139e4e --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -0,0 +1,159 @@ +package com.tangem.features.send.impl.presentation.state.fee.custom + +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.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +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 kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal +import java.math.RoundingMode + +internal class EthereumCustomFeeConverter( + private val clickIntents: SendClickIntents, + private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter> { + + override fun convert(value: Fee.Ethereum): ImmutableList { + return persistentListOf( + SendTextField.CustomFee( + value = value.amount.value?.parseBigDecimal(value.amount.decimals).orEmpty(), + decimals = value.amount.decimals, + symbol = value.amount.currencySymbol, + onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + title = resourceReference(R.string.send_max_fee), + footer = resourceReference(R.string.send_max_fee_footer), + label = getFeeFormatted(value.amount.value), + ), + SendTextField.CustomFee( + value = value.gasPrice.toString(), + decimals = ETHEREUM_GAS_DECIMALS, + symbol = ETHEREUM_GAS_UNIT, + title = resourceReference(R.string.send_gas_price), + footer = resourceReference(R.string.send_gas_price_footer), + onValueChange = { clickIntents.onCustomFeeValueChange(GAS_PRICE, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + ), + SendTextField.CustomFee( + value = value.gasLimit.toString(), + decimals = GAS_DECIMALS, + symbol = null, + title = resourceReference(R.string.send_gas_limit), + footer = resourceReference(R.string.send_gas_limit_footer), + onValueChange = { clickIntents.onCustomFeeValueChange(GAS_LIMIT, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Number, + ), + ), + ) + } + + fun convertBack(normalFee: Fee.Ethereum, value: ImmutableList): Fee.Ethereum { + val feeAmount = value[FEE_AMOUNT].value.parseToBigDecimal(value[FEE_AMOUNT].decimals) + val gasPrice = value[GAS_PRICE].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger() + val gasLimit = value[GAS_LIMIT].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger() + return normalFee.copy( + amount = normalFee.amount.copy(value = feeAmount), + gasPrice = gasPrice, + gasLimit = gasLimit, + ) + } + + fun onValueChange( + customValues: ImmutableList, + index: Int, + value: String, + ): ImmutableList { + val mutableCustomValues = customValues.toMutableList() + return mutableCustomValues.apply { + val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals) + when (index) { + FEE_AMOUNT -> { + val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals) + val newFeeAmount = newFeeAmountDecimal.movePointRight(this[FEE_AMOUNT].decimals) + val newGasPrice = newFeeAmount.divide(gasLimit, GAS_DECIMALS, RoundingMode.HALF_UP) + set(GAS_PRICE, this[GAS_PRICE].copy(value = newGasPrice.parseBigDecimal(GAS_DECIMALS))) + set( + index, + this[index].copy( + value = value, + label = getFeeFormatted(newFeeAmountDecimal), + ), + ) + } + GAS_PRICE -> { + val newGasPrice = value.parseToBigDecimal(this[GAS_PRICE].decimals) + .movePointLeft(this[GAS_PRICE].decimals) + val newFeeAmount = gasLimit * newGasPrice + set( + FEE_AMOUNT, + this[FEE_AMOUNT].copy( + value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals), + label = getFeeFormatted(newFeeAmount), + ), + ) + set(index, this[index].copy(value = value)) + } + else -> { + val newGasLimit = value.parseToBigDecimal(this[GAS_LIMIT].decimals) + val gasPrice = this[GAS_PRICE].value.parseToBigDecimal(this[GAS_PRICE].decimals) + .movePointLeft(this[FEE_AMOUNT].decimals) + val newFeeAmount = newGasLimit * gasPrice + set( + FEE_AMOUNT, + this[FEE_AMOUNT].copy( + value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals), + label = getFeeFormatted(newFeeAmount), + ), + ) + set(index, this[index].copy(value = value)) + } + } + }.toImmutableList() + } + + private fun getFeeFormatted(fee: BigDecimal?): TextReference { + val appCurrency = appCurrencyProvider() + val rate = cryptoCurrencyStatusProvider().value.fiatRate + val fiatFee = rate?.let { fee?.multiply(it) } + return stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatFee, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ) + } + + companion object { + private const val ETHEREUM_GAS_UNIT = "GWEI" + private const val ETHEREUM_GAS_DECIMALS = 18 + private const val FEE_AMOUNT = 0 + private const val GAS_PRICE = 1 + private const val GAS_LIMIT = 2 + private const val GAS_DECIMALS = 0 + } +} \ 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 80f5e20cac..b02ffb58d7 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 @@ -38,7 +38,10 @@ 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.* +import com.tangem.features.send.impl.presentation.state.fee.FeeNotificationFactory +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.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -71,7 +74,7 @@ internal class SendViewModel @Inject constructor( private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, - private val isFeeApproximateUseCase: IsFeeApproximateUseCase, + isFeeApproximateUseCase: IsFeeApproximateUseCase, getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, validateWalletMemoUseCase: ValidateWalletMemoUseCase, getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, @@ -536,7 +539,7 @@ internal class SendViewModel @Inject constructor( val feeState = uiState.feeState ?: return val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return val memo = uiState.recipientState?.memoTextField?.value - val fee = feeSelectorState.getFee() + val fee = feeStateFactory.feeConverter.convert(feeSelectorState) val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return val amountToSend = if (feeState.isSubtract && isAmountSubtractAvailable) { feeState.receivedAmountValue @@ -555,6 +558,7 @@ internal class SendViewModel @Inject constructor( ).fold( ifLeft = { Timber.e(it) + uiState = stateFactory.getSendingStateUpdate(isSending = false) uiState = eventStateFactory.getGenericErrorState( error = it, onConsume = { uiState = eventStateFactory.onConsumeEventState() }, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 4f8cc49e6f..f6b9405dcf 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-470" +tangemBlockchainSdk = "develop-471" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-324" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 1105a6c15902125691ceb0088d2d49fc2a0f2921 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 2 Feb 2024 09:29:19 +0300 Subject: [PATCH 23/28] Updated on 2026-08-14 --- core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt | 1 - .../src/main/java/com/tangem/core/ui/res/TangemTypography.kt | 4 ++-- .../feature/wallet/presentation/common/component/TokenItem.kt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 7be8761543..e68dfd96a0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -51,7 +51,6 @@ data class TangemDimens internal constructor( val size11: Dp = 11.dp, val size12: Dp = 12.dp, val size14: Dp = 14.dp, - val size15: Dp = 15.dp, val size16: Dp = 16.dp, val size18: Dp = 18.dp, val size20: Dp = 20.dp, diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt index 0187255098..eb48e1cd50 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt @@ -57,7 +57,7 @@ data class TangemTypography internal constructor( fontSize = 14.sp, fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 0.5f, type = TextUnitType.Sp), - lineHeight = TextUnit(value = 24f, type = TextUnitType.Sp), + lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp), ), val body1: TextStyle = TextStyle( fontFamily = RobotoFamily, @@ -78,7 +78,7 @@ data class TangemTypography internal constructor( fontSize = 14.sp, fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp), - lineHeight = TextUnit(value = 16f, type = TextUnitType.Sp), + lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp), ), val caption1: TextStyle = TextStyle( fontFamily = RobotoFamily, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 724291e0bd..9c6177c05e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -127,7 +127,7 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier val layoutWidth = constraints.maxWidth val horizontalPadding = with(density) { dimens.size14.roundToPx() } - val verticalPadding = with(density) { dimens.size15.roundToPx() } + val verticalPadding = with(density) { dimens.size16.roundToPx() } val layoutWidthWithoutPaddings = layoutWidth - 2 * horizontalPadding val titleMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt() From 9b6de19ebe6ef7948e5563e0d4b3cde7f4bc739e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 2 Feb 2024 15:38:53 +0400 Subject: [PATCH 24/28] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../tangem/tap/di/domain/VisaDomainModule.kt | 18 ++++ data/visa/.gitignore | 1 + data/visa/build.gradle.kts | 21 +++++ .../tangem/data/visa/DummyVisaRepository.kt | 12 +++ .../com/tangem/data/visa/di/VisaDataModule.kt | 20 +++++ .../domain/common/TangemCardTypesResolver.kt | 6 +- domain/visa/.gitignore | 1 + domain/visa/build.gradle.kts | 22 +++++ .../domain/visa/GetVisaCurrencyUseCase.kt | 18 ++++ .../tangem/domain/visa/model/VisaCurrency.kt | 32 +++++++ .../domain/visa/repository/VisaRepository.kt | 9 ++ features/wallet/impl/build.gradle.kts | 1 + .../implementors/VisaWalletContentLoader.kt | 19 ++-- .../VisaWalletContentLoaderFactory.kt | 12 +-- .../model/BalancesAndLimitsBlockState.kt | 1 - .../SetBalancesAndLimitsTransformer.kt | 88 +++++++++++++++--- .../SetPrimaryCurrencyTransformer.kt | 17 +--- .../BalancesAndLimitsBottomSheetConverter.kt | 52 +++++++++++ .../converter/VisaWalletCardStateConverter.kt | 89 ------------------- .../wallet/subscribers/TxHistorySubscriber.kt | 26 +++++- .../VisaWalletBalancesAndLimitsSubscriber.kt | 23 +++-- .../components/visa/BalancesAndLimitsBlock.kt | 13 +-- .../viewmodels/intents/VisaWalletIntents.kt | 72 ++++++--------- settings.gradle.kts | 2 + 25 files changed, 376 insertions(+), 201 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/VisaDomainModule.kt create mode 100644 data/visa/.gitignore create mode 100644 data/visa/build.gradle.kts create mode 100644 data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt create mode 100644 domain/visa/.gitignore create mode 100644 domain/visa/build.gradle.kts create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/visa/GetVisaCurrencyUseCase.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaRepository.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/VisaWalletCardStateConverter.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 834e2bdd48..8c5969e599 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.transaction) implementation(projects.domain.analytics) + implementation(projects.domain.visa) implementation(projects.common) implementation(projects.core.analytics) @@ -78,6 +79,7 @@ dependencies { implementation(projects.data.wallets) implementation(projects.data.analytics) implementation(projects.data.transaction) + implementation(projects.data.visa) /** Features */ implementation(projects.features.onboarding) diff --git a/app/src/main/java/com/tangem/tap/di/domain/VisaDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/VisaDomainModule.kt new file mode 100644 index 0000000000..61170463f0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/VisaDomainModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.visa.GetVisaCurrencyUseCase +import com.tangem.domain.visa.repository.VisaRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +internal object VisaDomainModule { + + @Provides + fun provideVisaCurrencyUseCase(visaRepository: VisaRepository): GetVisaCurrencyUseCase { + return GetVisaCurrencyUseCase(visaRepository) + } +} \ No newline at end of file diff --git a/data/visa/.gitignore b/data/visa/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data/visa/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts new file mode 100644 index 0000000000..e57696df79 --- /dev/null +++ b/data/visa/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.visa" +} + +dependencies { + + /** Project - Domain */ + implementation(projects.domain.visa) + implementation(projects.domain.wallets.models) + + /** DI */ + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt new file mode 100644 index 0000000000..40815fac3c --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt @@ -0,0 +1,12 @@ +package com.tangem.data.visa + +import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.domain.visa.repository.VisaRepository +import com.tangem.domain.wallets.models.UserWalletId + +internal class DummyVisaRepository : VisaRepository { + + override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency { + TODO(reason = "Implement in [REDACTED_JIRA]") + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt new file mode 100644 index 0000000000..ab68249dd9 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt @@ -0,0 +1,20 @@ +package com.tangem.data.visa.di + +import com.tangem.data.visa.DummyVisaRepository +import com.tangem.domain.visa.repository.VisaRepository +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 VisaDataModule { + + @Provides + @Singleton + fun provideVisaRepository(): VisaRepository { + return DummyVisaRepository() + } +} \ No newline at end of file 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 db6be16a4d..077ffe5de1 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 @@ -72,7 +72,10 @@ internal class TangemCardTypesResolver( override fun isSingleWalletWithToken(): Boolean = walletData?.token != null && !isMultiwalletAllowed() override fun isMultiwalletAllowed(): Boolean { - return !isTangemTwins() && !card.isStart2Coin && !isTangemNote() && + return !isTangemTwins() && + !card.isStart2Coin && + !isTangemNote() && + !isVisaWallet() && (multiWalletAvailable() || card.wallets.firstOrNull()?.curve == EllipticCurve.Secp256k1) } @@ -81,6 +84,7 @@ internal class TangemCardTypesResolver( override fun getBlockchain(): Blockchain { return when (productType) { ProductType.Start2Coin -> if (card.isTestCard) Blockchain.BitcoinTestnet else Blockchain.Bitcoin + ProductType.Visa -> Blockchain.PolygonTestnet else -> { val blockchainName: String = walletData?.blockchain ?: if (productType == ProductType.Note) { diff --git a/domain/visa/.gitignore b/domain/visa/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/visa/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts new file mode 100644 index 0000000000..64262e878a --- /dev/null +++ b/domain/visa/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.visa" +} + +dependencies { + + /** Project - Domain */ + implementation(projects.core.utils) + implementation(projects.domain.core) + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.appCurrency.models) + + /** Libs - Other */ + implementation(deps.jodatime) +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/GetVisaCurrencyUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/GetVisaCurrencyUseCase.kt new file mode 100644 index 0000000000..d5b71eff26 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/GetVisaCurrencyUseCase.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.visa + +import arrow.core.Either +import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.domain.visa.repository.VisaRepository +import com.tangem.domain.wallets.models.UserWalletId + +class GetVisaCurrencyUseCase( + private val repository: VisaRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + isRefresh: Boolean = false, + ): Either { + return Either.catch { repository.getVisaCurrency(userWalletId, isRefresh) } + } +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt new file mode 100644 index 0000000000..1e6a0c55a1 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt @@ -0,0 +1,32 @@ +package com.tangem.domain.visa.model + +import com.tangem.domain.appcurrency.model.AppCurrency +import org.joda.time.DateTime +import java.math.BigDecimal + +data class VisaCurrency( + val networkName: String, + val symbol: String, + val decimals: Int, + val fiatRate: BigDecimal?, + val fiatCurrency: AppCurrency, + val balances: Balances, + val limits: Limits, +) { + + data class Balances( + val total: BigDecimal, + val verified: BigDecimal, + val available: BigDecimal, + val blocked: BigDecimal, + val debt: BigDecimal, + val pendingRefund: BigDecimal, + ) + + data class Limits( + val remainingOtp: BigDecimal, + val remainingNoOtp: BigDecimal, + val singleTransaction: BigDecimal, + val expirationDate: DateTime, + ) +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaRepository.kt new file mode 100644 index 0000000000..96d6efd7fd --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaRepository.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.visa.repository + +import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.domain.wallets.models.UserWalletId + +interface VisaRepository { + + suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean = false): VisaCurrency +} \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index decfa1c37e..a24c30a994 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -69,6 +69,7 @@ dependencies { implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.analytics) + implementation(projects.domain.visa) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoader.kt index 8a221a2d24..7d41dcb257 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoader.kt @@ -1,14 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.visa.GetVisaCurrencyUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.subscribers.PrimaryCurrencySubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.TxHistorySubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.VisaWalletBalancesAndLimitsSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber @@ -21,24 +18,20 @@ internal class VisaWalletContentLoader( private val isRefresh: Boolean, private val stateHolder: WalletStateController, private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, - private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, + private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { return listOf( - PrimaryCurrencySubscriber( + VisaWalletBalancesAndLimitsSubscriber( userWallet = userWallet, stateHolder = stateHolder, - getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, - setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, - analyticsEventHandler = analyticsEventHandler, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, + isRefresh = isRefresh, + getVisaCurrencyUseCase = getVisaCurrencyUseCase, + clickIntents = clickIntents, ), - VisaWalletBalancesAndLimitsSubscriber(userWallet, stateHolder, clickIntents), TxHistorySubscriber( userWallet = userWallet, isRefresh = isRefresh, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoaderFactory.kt index 61db482fa2..e81f30afff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoaderFactory.kt @@ -1,11 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.visa.GetVisaCurrencyUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateController import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 @@ -17,11 +15,9 @@ import javax.inject.Inject internal class VisaWalletContentLoaderFactory @Inject constructor( private val stateHolder: WalletStateController, private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, - private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, + private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntentsV2, isRefresh: Boolean): WalletContentLoader { @@ -31,11 +27,9 @@ internal class VisaWalletContentLoaderFactory @Inject constructor( isRefresh = isRefresh, stateHolder = stateHolder, getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, - setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, txHistoryItemsUseCase = txHistoryItemsUseCase, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - analyticsEventHandler = analyticsEventHandler, + getVisaCurrencyUseCase = getVisaCurrencyUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/model/BalancesAndLimitsBlockState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/model/BalancesAndLimitsBlockState.kt index 883ce0b468..a64e659900 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/model/BalancesAndLimitsBlockState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/model/BalancesAndLimitsBlockState.kt @@ -11,7 +11,6 @@ internal sealed class BalancesAndLimitsBlockState { data class Content( val availableBalance: String, - val currencySymbol: String, val limitDays: Int, val isEnabled: Boolean, val onClick: () -> Unit, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetBalancesAndLimitsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetBalancesAndLimitsTransformer.kt index 4092298ae5..9af5df2c08 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetBalancesAndLimitsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetBalancesAndLimitsTransformer.kt @@ -1,32 +1,98 @@ package com.tangem.feature.wallet.presentation.wallet.state2.transformers +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletAdditionalInfo +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state2.model.BalancesAndLimitsBlockState import com.tangem.feature.wallet.presentation.wallet.state2.model.WalletState import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import org.joda.time.DateTime +import org.joda.time.Days internal class SetBalancesAndLimitsTransformer( - userWallet: UserWallet, + private val userWallet: UserWallet, + private val maybeVisaCurrency: Either, private val clickIntents: WalletClickIntentsV2, ) : WalletStateTransformer(userWallet.walletId) { override fun transform(prevState: WalletState): WalletState { return prevState.transformWhenInState { state -> + val visaCurrency = maybeVisaCurrency.getOrElse { + return state.copy( + walletCardState = getErrorWalletCardState(state.walletCardState), + balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, + ) + } + state.copy( - balancesAndLimitBlockState = state.balancesAndLimitBlockState.toLoadedState(), + walletCardState = getContentWalletCardState(state.walletCardState, visaCurrency), + balancesAndLimitBlockState = getContentBlockState(visaCurrency), ) } } - // TODO: Implement in [REDACTED_JIRA] - @Suppress("UnusedReceiverParameter") - private fun BalancesAndLimitsBlockState.toLoadedState(): BalancesAndLimitsBlockState { - return BalancesAndLimitsBlockState.Content( - availableBalance = "400.00", - currencySymbol = "USDT", - limitDays = 7, - isEnabled = true, - onClick = clickIntents::onBalancesAndLimitsClick, + private fun getContentBlockState(visaCurrency: VisaCurrency) = BalancesAndLimitsBlockState.Content( + availableBalance = BigDecimalFormatter.formatCryptoAmount( + visaCurrency.limits.remainingOtp, + visaCurrency.symbol, + visaCurrency.decimals, + ), + limitDays = Days.daysBetween(DateTime.now(), visaCurrency.limits.expirationDate).days.inc(), + isEnabled = true, + onClick = clickIntents::onBalancesAndLimitsClick, + ) + + private fun getErrorWalletCardState(prevState: WalletCardState): WalletCardState { + return with(prevState) { + WalletCardState.Error( + id = id, + title = title, + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + ) + } + } + + private fun getContentWalletCardState(prevState: WalletCardState, visaCurrency: VisaCurrency): WalletCardState { + return with(prevState) { + WalletCardState.Content( + id = id, + title = title, + additionalInfo = createAdditionalInfo(visaCurrency), + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + balance = BigDecimalFormatter.formatCryptoAmount( + visaCurrency.balances.available, + visaCurrency.symbol, + visaCurrency.decimals, + ), + cardCount = userWallet.getCardsCount(), + ) + } + } + + private fun createAdditionalInfo(visaCurrency: VisaCurrency): WalletAdditionalInfo { + val fiatAmount = BigDecimalFormatter.formatFiatAmount( + fiatAmount = visaCurrency.fiatRate?.let { visaCurrency.balances.available.multiply(it) }, + fiatCurrencyCode = visaCurrency.fiatCurrency.code, + fiatCurrencySymbol = visaCurrency.fiatCurrency.symbol, ) + val infoContent = stringReference( + value = buildString { + append(fiatAmount) + append(" • ") + append(visaCurrency.networkName) + }, + ) + + return WalletAdditionalInfo(hideable = true, infoContent) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt index ade63f113c..da21391c07 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt @@ -8,7 +8,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCard import com.tangem.feature.wallet.presentation.wallet.state2.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.SingleWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.SingleWalletMarketPriceConverter -import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.VisaWalletCardStateConverter import timber.log.Timber internal class SetPrimaryCurrencyTransformer( @@ -25,15 +24,11 @@ internal class SetPrimaryCurrencyTransformer( marketPriceBlockState = prevState.marketPriceBlockState.toLoadedState(), ) } - is WalletState.Visa.Content -> { - prevState.copy( - walletCardState = prevState.walletCardState.toLoadedVisaState(), - depositButtonState = prevState.depositButtonState.copy(isEnabled = true), - ) + is WalletState.Visa -> { + Timber.w("Impossible to load primary currency status for VISA wallet") + prevState } - is WalletState.Visa.Locked, - is WalletState.SingleCurrency.Locked, - -> { + is WalletState.SingleCurrency.Locked -> { Timber.w("Impossible to load primary currency status for locked wallet") prevState } @@ -48,10 +43,6 @@ internal class SetPrimaryCurrencyTransformer( return SingleWalletCardStateConverter(status.value, userWallet, appCurrency).convert(value = this) } - private fun WalletCardState.toLoadedVisaState(): WalletCardState { - return VisaWalletCardStateConverter(status, userWallet, appCurrency).convert(value = this) - } - private fun MarketPriceBlockState.toLoadedState(): MarketPriceBlockState { return SingleWalletMarketPriceConverter(status.value, appCurrency).convert(value = this) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt new file mode 100644 index 0000000000..2a6139aa60 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt @@ -0,0 +1,52 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.model.BalancesAndLimitsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class BalancesAndLimitsBottomSheetConverter( + private val eventSender: WalletEventSender, +) : Converter { + + override fun convert(value: VisaCurrency): BalancesAndLimitsBottomSheetConfig { + return BalancesAndLimitsBottomSheetConfig( + currency = value.symbol, + balance = BalancesAndLimitsBottomSheetConfig.Balance( + totalBalance = value.balances.total.let(::formatAmount), + availableBalance = value.balances.available.let(::formatAmount), + blockedBalance = value.balances.blocked.let(::formatAmount), + debit = value.balances.debt.let(::formatAmount), + pending = value.balances.pendingRefund.let(::formatAmount), + amlVerified = value.balances.verified.let(::formatAmount), + ), + limit = BalancesAndLimitsBottomSheetConfig.Limit( + availableBy = DateTimeFormatters.formatDate(date = value.limits.expirationDate), + inStore = value.limits.remainingOtp.let(::formatAmount), + other = value.limits.remainingNoOtp.let(::formatAmount), + singleTransaction = value.limits.singleTransaction.let(::formatAmount), + ), + onBalanceInfoClick = this::showBalanceInfo, + onLimitInfoClick = this::showLimitInfo, + ) + } + + private fun formatAmount(amount: BigDecimal): String = BigDecimalFormatter.formatCryptoAmount( + amount, + cryptoCurrency = "", + decimals = 2, + ) + + private fun showBalanceInfo() { + eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaBalancesInfo)) + } + + private fun showLimitInfo() { + eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo)) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/VisaWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/VisaWalletCardStateConverter.kt deleted file mode 100644 index 56766efc40..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/VisaWalletCardStateConverter.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter - -import com.tangem.core.ui.extensions.stringReference -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.feature.wallet.presentation.wallet.domain.getCardsCount -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletAdditionalInfo -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState -import com.tangem.utils.converter.Converter - -internal class VisaWalletCardStateConverter( - private val status: CryptoCurrencyStatus, - private val selectedWallet: UserWallet, - private val appCurrency: AppCurrency, -) : Converter { - - override fun convert(value: WalletCardState): WalletCardState { - return when (status.value) { - is CryptoCurrencyStatus.Loading -> value.toLoadingState() - is CryptoCurrencyStatus.Custom, - is CryptoCurrencyStatus.MissedDerivation, - is CryptoCurrencyStatus.Unreachable, - -> value.toErrorState() - is CryptoCurrencyStatus.NoQuote, - is CryptoCurrencyStatus.Loaded, - is CryptoCurrencyStatus.NoAccount, - is CryptoCurrencyStatus.NoAmount, - -> value.toContentState(status) - } - } - - private fun WalletCardState.toLoadingState(): WalletCardState { - return WalletCardState.Loading( - id = id, - title = title, - imageResId = imageResId, - onRenameClick = onRenameClick, - onDeleteClick = onDeleteClick, - ) - } - - private fun WalletCardState.toErrorState(): WalletCardState { - return WalletCardState.Error( - id = id, - title = title, - imageResId = imageResId, - onRenameClick = onRenameClick, - onDeleteClick = onDeleteClick, - ) - } - - private fun WalletCardState.toContentState(status: CryptoCurrencyStatus): WalletCardState { - return WalletCardState.Content( - id = id, - title = title, - additionalInfo = createAdditionalInfo(status), - imageResId = imageResId, - onRenameClick = onRenameClick, - onDeleteClick = onDeleteClick, - balance = formatAmount(status), - cardCount = selectedWallet.getCardsCount(), - ) - } - - private fun createAdditionalInfo(status: CryptoCurrencyStatus): WalletAdditionalInfo { - val fiatAmount = BigDecimalFormatter.formatFiatAmount( - status.value.fiatAmount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - val infoContent = stringReference( - value = buildString { - append(fiatAmount) - append(" • ") - append(status.currency.network.name) - }, - ) - - return WalletAdditionalInfo(hideable = true, infoContent) - } - - private fun formatAmount(status: CryptoCurrencyStatus): String { - val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN - - return BigDecimalFormatter.formatCryptoAmount(amount, status.currency) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt index a1e47bf291..d9d435e6f0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt @@ -3,6 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import androidx.paging.PagingData import androidx.paging.cachedIn import arrow.core.Either +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.models.TxHistoryItem @@ -13,10 +15,8 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryCountErrorTransformer -import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryCountTransformer -import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryItemsErrorTransformer -import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryItemsTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.* import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -37,6 +37,24 @@ internal class TxHistorySubscriber( ) : WalletSubscriber() { override fun create(coroutineScope: CoroutineScope): Flow> { + // TODO: [REDACTED_JIRA] + if (userWallet.scanResponse.cardTypesResolver.isVisaWallet()) { + return flow { + stateHolder.update( + object : WalletStateTransformer(userWallet.walletId) { + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.Visa.Content -> prevState.copy( + txHistoryState = TxHistoryState.Empty(onExploreClick = {}), + ) + else -> prevState + } + } + }, + ) + } + } + return flow { getPrimaryCurrencyStatusUpdatesUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> val maybeTxHistoryItemCount = txHistoryItemsCountUseCase( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletBalancesAndLimitsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletBalancesAndLimitsSubscriber.kt index af94827eb2..a25af15cdc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletBalancesAndLimitsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletBalancesAndLimitsSubscriber.kt @@ -1,23 +1,32 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.domain.visa.GetVisaCurrencyUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetBalancesAndLimitsTransformer import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flow +@Suppress("LongParameterList") internal class VisaWalletBalancesAndLimitsSubscriber( private val userWallet: UserWallet, private val stateHolder: WalletStateController, + private val isRefresh: Boolean, + private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase, private val clickIntents: WalletClickIntentsV2, ) : WalletSubscriber() { - // TODO: Implement in [REDACTED_JIRA] - override fun create(coroutineScope: CoroutineScope): Flow<*> = suspend { - delay(timeMillis = 500) - stateHolder.update(SetBalancesAndLimitsTransformer(userWallet, clickIntents)) - }.asFlow() + override fun create(coroutineScope: CoroutineScope): Flow<*> { + return flow { + stateHolder.update( + SetBalancesAndLimitsTransformer( + userWallet = userWallet, + maybeVisaCurrency = getVisaCurrencyUseCase(userWallet.walletId, isRefresh), + clickIntents = clickIntents, + ), + ) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt index 26ede92649..5855ab22f6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt @@ -113,7 +113,6 @@ private fun Content(state: BalancesAndLimitsBlockState, modifier: Modifier = Mod is BalancesAndLimitsBlockState.Content -> with(blockState) { AvailableLimit( availableBalance = availableBalance, - currencySymbol = currencySymbol, limitDays = limitDays, ) } @@ -136,19 +135,14 @@ private fun Content(state: BalancesAndLimitsBlockState, modifier: Modifier = Mod } @Composable -private fun AvailableLimit( - availableBalance: String, - currencySymbol: String, - limitDays: Int, - modifier: Modifier = Modifier, -) { +private fun AvailableLimit(availableBalance: String, limitDays: Int, modifier: Modifier = Modifier) { Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { Text( - text = "$availableBalance $currencySymbol", + text = availableBalance, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, ) @@ -186,8 +180,7 @@ private class BalancesAndLimitsBlockParameterProvider : CollectionPreviewParamet BalancesAndLimitsBlockState.Loading, BalancesAndLimitsBlockState.Error, BalancesAndLimitsBlockState.Content( - availableBalance = "400.00", - currencySymbol = "USDT", + availableBalance = "400.00 USDT", limitDays = 7, isEnabled = true, onClick = {}, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/VisaWalletIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/VisaWalletIntents.kt index fc0b879db4..5dad048ccd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/VisaWalletIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/VisaWalletIntents.kt @@ -4,18 +4,16 @@ import arrow.core.getOrElse import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.visa.GetVisaCurrencyUseCase import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState -import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state2.model.BalancesAndLimitsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.BalancesAndLimitsBottomSheetConverter import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -31,10 +29,15 @@ internal interface VisaWalletIntents { internal class VisaWalletIntentsImplementor @Inject constructor( private val stateController: WalletStateController, private val eventSender: WalletEventSender, - private val getPrimaryCurrencyUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase, + private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase, private val dispatchers: CoroutineDispatcherProvider, ) : BaseWalletClickIntents(), VisaWalletIntents { + private val balancesAndLimitsBottomSheetConverter by lazy(mode = LazyThreadSafetyMode.NONE) { + BalancesAndLimitsBottomSheetConverter(eventSender) + } + override fun onDepositClick() { val userWalletId = stateController.getSelectedWalletId() @@ -47,15 +50,6 @@ internal class VisaWalletIntentsImplementor @Inject constructor( } } - private suspend fun getPrimaryCurrencyStatus(userWalletId: UserWalletId): CryptoCurrencyStatus? { - return getPrimaryCurrencyUseCase(userWalletId) - .firstOrNull() - ?.getOrElse { - Timber.e("Failed to get primary currency $it") - null - } - } - private fun createReceiveBottomSheetContent(currencyStatus: CryptoCurrencyStatus): TangemBottomSheetConfigContent? { val currency = currencyStatus.currency val addresses = currencyStatus.value.networkAddress?.availableAddresses @@ -76,35 +70,27 @@ internal class VisaWalletIntentsImplementor @Inject constructor( } override fun onBalancesAndLimitsClick() { - stateController.showBottomSheet(getBalancesAndLimitsConfig()) + viewModelScope.launch(dispatchers.main) { + val userWalletId = stateController.getSelectedWalletId() + val balancesAndLimits = getVisaCurrencyUseCase(userWalletId) + .getOrElse { + Timber.e("Unable to get balances and limits: $it") + return@launch + } + + val bottomSheetContent = balancesAndLimitsBottomSheetConverter.convert( + value = balancesAndLimits, + ) + + stateController.showBottomSheet(bottomSheetContent) + } } - // TODO: Implement - private fun getBalancesAndLimitsConfig() = BalancesAndLimitsBottomSheetConfig( - currency = "USDT", - balance = BalancesAndLimitsBottomSheetConfig.Balance( - totalBalance = "492.45", - availableBalance = "392.45", - blockedBalance = "36.00", - debit = "00.00", - pending = "20.99", - amlVerified = "356.45", - ), - limit = BalancesAndLimitsBottomSheetConfig.Limit( - availableBy = "Nov, 11", - inStore = "563.00", - other = "100.00", - singleTransaction = "100.00", - ), - onBalanceInfoClick = this::showBalanceInfo, - onLimitInfoClick = this::showLimitInfo, - ) - - private fun showBalanceInfo() { - eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaBalancesInfo)) - } - - private fun showLimitInfo() { - eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo)) + private suspend fun getPrimaryCurrencyStatus(userWalletId: UserWalletId): CryptoCurrencyStatus? { + return getCurrencyStatusUseCase(userWalletId) + .getOrElse { + Timber.e("Failed to get primary currency $it") + null + } } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 3ac4a3dcf4..7fc4e85cba 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -135,6 +135,7 @@ include(":domain:balance-hiding") include(":domain:balance-hiding:models") include(":domain:transaction") include(":domain:analytics") +include(":domain:visa") // endregion Domain modules // region Data modules @@ -150,4 +151,5 @@ include(":data:txhistory") include(":data:wallets") include(":data:analytics") include(":data:transaction") +include(":data:visa") // endregion Data modules \ No newline at end of file From ce602107bbf489b07f44e363e9df38c181c4a85f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 2 Feb 2024 10:12:14 +0300 Subject: [PATCH 25/28] Updated on 2026-08-14 --- .../com/tangem/tap/common/DialogManager.kt | 1 + .../tap/common/redux/global/GlobalAction.kt | 2 + .../tap/common/redux/global/GlobalReducer.kt | 5 + .../tap/common/redux/global/GlobalState.kt | 1 + .../com/tangem/tap/domain/TangemSdkManager.kt | 12 ++- .../domain/tasks/CreateWalletAndRescanTask.kt | 38 -------- .../tap/domain/tasks/CreateWalletsTask.kt | 49 ---------- .../product/CardInitializationValidator.kt | 13 +++ .../tasks/product/CreateProductWalletTask.kt | 96 +++++++++++++++---- .../features/onboarding/OnboardingDialog.kt | 1 + .../redux/OnboardingWalletMiddleware.kt | 38 +++++++- .../ui/OnboardingSeedPhraseStateHandler.kt | 4 +- .../ui/dialogs/WalletActivationErrorDialog.kt | 27 ++++++ core/res/src/main/res/values-ru/strings.xml | 2 + core/res/src/main/res/values/strings.xml | 2 + ...hrase.kt => OnboardingSeedPhraseScreen.kt} | 2 +- 16 files changed, 178 insertions(+), 115 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletAndRescanTask.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletsTask.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/tasks/product/CardInitializationValidator.kt create mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt rename features/onboarding/src/main/java/com/tangem/feature/onboarding/api/{OnboardingSeedPhrase.kt => OnboardingSeedPhraseScreen.kt} (97%) diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index a7da950412..4f4494f2bc 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -55,6 +55,7 @@ class DialogManager : StoreSubscriber { ) is OnboardingDialog.TwinningProcessNotCompleted -> TwinningProcessNotCompletedDialog.create(context) is OnboardingDialog.InterruptOnboarding -> InterruptOnboardingDialog.create(context, state.dialog) + is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog) is WalletConnectDialog.UnsupportedCard -> SimpleAlertDialog.create( titleRes = R.string.wallet_connect_title, diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index c2bd7ec6d8..e3b376b612 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -46,6 +46,8 @@ sealed class GlobalAction : Action { data class StartForUnfinishedBackup(val addedBackupCardsCount: Int) : Onboarding() object Stop : Onboarding() + + data class ShouldResetCardOnCreate(val shouldReset: Boolean) : Onboarding() } object ScanFailsCounter { diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 6556e4518b..bd03cf6cae 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -30,6 +30,11 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde is GlobalAction.Onboarding.Stop -> { globalState.copy(onboardingState = OnboardingState(false)) } + is GlobalAction.Onboarding.ShouldResetCardOnCreate -> { + globalState.copy( + onboardingState = globalState.onboardingState.copy(shouldResetOnCreate = action.shouldReset), + ) + } is GlobalAction.ScanFailsCounter.Increment -> { globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 5cdc02d930..20712881c8 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -36,4 +36,5 @@ typealias CryptoCurrencyName = String data class OnboardingState( val onboardingStarted: Boolean = false, val onboardingManager: OnboardingManager? = null, + val shouldResetOnCreate: Boolean = false, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 33ea89804c..d9e620497a 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -85,11 +85,15 @@ class TangemSdkManager( ).also { sendScanResultsToAnalytics(it) } } - suspend fun createProductWallet(scanResponse: ScanResponse): CompletionResult { + suspend fun createProductWallet( + scanResponse: ScanResponse, + shouldReset: Boolean = false, + ): CompletionResult { return runTaskAsync( runnable = CreateProductWalletTask( cardTypesResolver = scanResponse.cardTypesResolver, derivationStyleProvider = scanResponse.derivationStyleProvider, + shouldReset = shouldReset, ), cardId = scanResponse.card.cardId, initialMessage = Message(resources.getString(R.string.initial_message_create_wallet_body)), @@ -100,6 +104,7 @@ class TangemSdkManager( suspend fun importWallet( scanResponse: ScanResponse, mnemonic: String, + shouldReset: Boolean, ): CompletionResult { val defaultMnemonic = try { DefaultMnemonic(mnemonic, tangemSdk.wordlist) @@ -108,9 +113,10 @@ class TangemSdkManager( } return runTaskAsync( CreateProductWalletTask( - scanResponse.cardTypesResolver, + cardTypesResolver = scanResponse.cardTypesResolver, derivationStyleProvider = scanResponse.derivationStyleProvider, - defaultMnemonic, + mnemonic = defaultMnemonic, + shouldReset = shouldReset, ), scanResponse.card.cardId, Message(resources.getString(R.string.initial_message_create_wallet_body)), diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletAndRescanTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletAndRescanTask.kt deleted file mode 100644 index 81a967c20b..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletAndRescanTask.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.tap.domain.tasks - -import com.tangem.common.CompletionResult -import com.tangem.common.card.Card -import com.tangem.common.card.FirmwareVersion -import com.tangem.common.core.CardSession -import com.tangem.common.core.CardSessionRunnable -import com.tangem.common.core.TangemSdkError -import com.tangem.common.extensions.guard -import com.tangem.operations.PreflightReadMode -import com.tangem.operations.PreflightReadTask -import com.tangem.operations.wallet.CreateWalletTask - -@Deprecated("Use CreateProductWalletAndRescanTask instead") -class CreateWalletAndRescanTask : CardSessionRunnable { - - override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { - val card = session.environment.card.guard { - callback(CompletionResult.Failure(TangemSdkError.CardError())) - return - } - val firmwareVersion = card.firmwareVersion - - val task = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) { - CreateWalletTask(card.supportedCurves.first()) - } else { - CreateWalletsTask() - } - - task.run(session) { result -> - when (result) { - is CompletionResult.Success -> - PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback) - is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletsTask.kt deleted file mode 100644 index 260736b303..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletsTask.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.tangem.tap.domain.tasks - -import com.tangem.common.CompletionResult -import com.tangem.common.card.Card -import com.tangem.common.card.EllipticCurve -import com.tangem.common.core.CardSession -import com.tangem.common.core.CardSessionRunnable -import com.tangem.operations.PreflightReadMode -import com.tangem.operations.PreflightReadTask -import com.tangem.operations.wallet.CreateWalletTask - -@Deprecated("Use CreateProductWalletTask instead") -class CreateWalletsTask(curves: List? = null) : CardSessionRunnable { - - private val curves = curves ?: listOf( - EllipticCurve.Secp256k1, - EllipticCurve.Ed25519, - EllipticCurve.Secp256r1, - ) - - private var index = 0 - - override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { - val curve = curves[index] - createWallet(curve, session, callback) - } - - private fun createWallet( - curve: EllipticCurve, - session: CardSession, - callback: (result: CompletionResult) -> Unit, - ) { - CreateWalletTask(curve).run(session) { result -> - when (result) { - is CompletionResult.Success -> { - if (index == curves.lastIndex) { - PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback) - return@run - } - index += 1 - createWallet(curves[index], session, callback) - } - is CompletionResult.Failure -> { - callback(CompletionResult.Failure(result.error)) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CardInitializationValidator.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CardInitializationValidator.kt new file mode 100644 index 0000000000..624e1d3abe --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CardInitializationValidator.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.domain.tasks.product + +import com.tangem.common.card.CardWallet +import com.tangem.common.card.EllipticCurve + +class CardInitializationValidator(private val expectedCurves: List) { + + fun validateWallets(wallets: List): Boolean { + val curves = wallets.map { it.curve }.toSet() + return curves.size == expectedCurves.size && + curves.containsAll(expectedCurves) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index 48e5ed9a7a..f5c1a21f38 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.common.card.Card import com.tangem.common.card.EllipticCurve +import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.TangemSdkError @@ -24,6 +25,7 @@ import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.backup.StartPrimaryCardLinkingCommand import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.operations.read.ReadWalletsListCommand import com.tangem.operations.wallet.CreateWalletTask import com.tangem.tap.features.demo.DemoHelper import com.tangem.operations.wallet.CreateWalletResponse as SdkCreateWalletResponse @@ -60,6 +62,7 @@ class CreateProductWalletTask( private val cardTypesResolver: CardTypesResolver, private val derivationStyleProvider: DerivationStyleProvider, private val mnemonic: Mnemonic? = null, + private val shouldReset: Boolean, ) : CardSessionRunnable { override val allowsRequestAccessCodeFromRepository: Boolean = false @@ -79,7 +82,7 @@ class CreateProductWalletTask( cardTypesResolver.isTangemTwins() -> throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet") - else -> CreateWalletTangemWallet(mnemonic, derivationStyleProvider) + else -> CreateWalletTangemWallet(mnemonic, shouldReset, derivationStyleProvider, cardDto) } commandProcessor.proceed(cardDto, session) { when (it) { @@ -139,38 +142,38 @@ private class CreateWalletTangemNote(private val cardTypesResolver: CardTypesRes */ private class CreateWalletTangemWallet( private val mnemonic: Mnemonic?, + private val shouldReset: Boolean, private val derivationStyleProvider: DerivationStyleProvider, + cardDTO: CardDTO, ) : ProductCommandProcessor { private var primaryCard: PrimaryCard? = null + private val cardConfig = CardConfig.createConfig(cardDTO) override fun proceed( card: CardDTO, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { - val config = CardConfig.createConfig(card) val walletsOnCard = card.wallets.map { it.curve }.toSet() - val curves = config.mandatoryCurves.toSet() - .intersect(card.supportedCurves.toSet()) - .subtract(walletsOnCard).toList() - - if (curves.isEmpty()) { - val createWalletResponses = card.wallets.map { wallet -> - CreateWalletResponse(card.cardId, wallet) - } - proceedWithCreatedWallets(card, createWalletResponses, session, callback) - return + if (walletsOnCard.isEmpty()) { + createMultiWallet(card, session, callback) + } else if (shouldReset) { + resetCard(card, session, callback) + } else { + callback(CompletionResult.Failure(TangemSdkError.WalletAlreadyCreated())) } - CreateWalletsTask(curves, mnemonic).run(session) { result -> + } + + private fun createMultiWallet( + card: CardDTO, + session: CardSession, + callback: (result: CompletionResult) -> Unit, + ) { + CreateWalletsTask(cardConfig.mandatoryCurves, mnemonic).run(session) { result -> when (result) { is CompletionResult.Success -> { - proceedWithCreatedWallets( - card = card, - createWalletResponses = result.data.createWalletResponses.map { CreateWalletResponse(it) }, - session = session, - callback = callback, - ) + checkIfAllWalletsCreated(card, session, result.data, callback) } is CompletionResult.Failure -> { callback(CompletionResult.Failure(result.error)) @@ -179,6 +182,61 @@ private class CreateWalletTangemWallet( } } + private fun checkIfAllWalletsCreated( + card: CardDTO, + session: CardSession, + createResponse: CreateWalletsResponse, + callback: (result: CompletionResult) -> Unit, + ) { + if (card.firmwareVersion < FirmwareVersion.MultiWalletAvailable) { + proceedWithCreatedWallets( + card = card, + createWalletResponses = createResponse.createWalletResponses.map { CreateWalletResponse(it) }, + session = session, + callback = callback, + ) + return + } + + val command = ReadWalletsListCommand() + command.run(session) { response -> + when (response) { + is CompletionResult.Success -> { + val cardInitializationValidator = CardInitializationValidator(cardConfig.mandatoryCurves) + if (cardInitializationValidator.validateWallets(response.data.wallets)) { + proceedWithCreatedWallets( + card = card, + createWalletResponses = createResponse.createWalletResponses.map { + CreateWalletResponse(it) + }, + session = session, + callback = callback, + ) + } else { + callback(CompletionResult.Failure(TangemSdkError.WalletAlreadyCreated())) + } + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error)) + } + } + } + + private fun resetCard( + card: CardDTO, + session: CardSession, + callback: (result: CompletionResult) -> Unit, + ) { + val resetCommand = ResetToFactorySettingsTask(allowsRequestAccessCodeFromRepository = false) + resetCommand.run(session) { + when (it) { + is CompletionResult.Success -> { + createMultiWallet(card, session, callback) + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(it.error)) + } + } + } + private fun proceedWithCreatedWallets( card: CardDTO, createWalletResponses: List, diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt index 901014d44b..0134ddaf15 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt @@ -9,4 +9,5 @@ import com.tangem.core.navigation.StateDialog sealed class OnboardingDialog : StateDialog { object TwinningProcessNotCompleted : OnboardingDialog() data class InterruptOnboarding(val onOk: VoidCallback) : OnboardingDialog() + data class WalletActivationError(val onConfirm: () -> Unit) : OnboardingDialog() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index bae2e62550..418ca44742 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -120,7 +120,10 @@ private fun handleWalletAction(action: Action) { is OnboardingWalletAction.CreateWallet -> { scanResponse ?: return scope.launch { - val result = tangemSdkManager.createProductWallet(scanResponse) + val result = tangemSdkManager.createProductWallet( + scanResponse, + globalState.onboardingState.shouldResetOnCreate, + ) store.dispatchOnMain(OnboardingWalletAction.WalletWasCreated(true, result)) } } @@ -139,10 +142,15 @@ private fun handleWalletAction(action: Action) { ) onboardingManager.scanResponse = updatedResponse + store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false)) startCardActivation(updatedResponse) store.dispatch(OnboardingWalletAction.ResumeBackup) } - is CompletionResult.Failure -> Unit + is CompletionResult.Failure -> { + if (result.error is TangemSdkError.WalletAlreadyCreated) { + handleActivationError() + } + } } } is OnboardingWalletAction.FinishOnboarding -> { @@ -218,9 +226,15 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { is OnboardingWallet2Action.CreateWallet -> { scanResponse ?: return scope.launch { - val mediateResult = when (val result = tangemSdkManager.createProductWallet(scanResponse)) { + val mediateResult = when ( + val result = tangemSdkManager.createProductWallet( + scanResponse, + globalState.onboardingState.shouldResetOnCreate, + ) + ) { is CompletionResult.Success -> { Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully()) + store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false)) val response = CreateWalletResponse( card = result.data.card, derivedKeys = result.data.derivedKeys, @@ -230,6 +244,9 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { } is CompletionResult.Failure -> { + if (result.error is TangemSdkError.WalletAlreadyCreated) { + handleActivationError() + } CompletionResult.Failure(result.error) } } @@ -246,6 +263,7 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { val result = tangemSdkManager.importWallet( scanResponse = scanResponse, mnemonic = action.mnemonicComponents.joinToString(" "), + shouldReset = globalState.onboardingState.shouldResetOnCreate, ) ) { is CompletionResult.Success -> { @@ -259,6 +277,7 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { seedPhraseLength = action.mnemonicComponents.size, ), ) + store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false)) val response = CreateWalletResponse( card = result.data.card, derivedKeys = result.data.derivedKeys, @@ -268,6 +287,9 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { } is CompletionResult.Failure -> { + if (result.error is TangemSdkError.WalletAlreadyCreated) { + handleActivationError() + } CompletionResult.Failure(result.error) } } @@ -300,6 +322,16 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { } } +private fun handleActivationError() { + store.dispatchDialogShow( + OnboardingDialog.WalletActivationError( + onConfirm = { + store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(true)) + }, + ), + ) +} + private fun updateScanResponseAfterBackup(scanResponse: ScanResponse, backupState: BackupState): ScanResponse { val card = if (backupState.backupCardsNumber > 0) { val cardsCount = backupState.backupCardsNumber diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt index 11f7664aa8..e92ebc5ca3 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.onboarding.products.wallet.ui import androidx.compose.runtime.collectAsState -import com.tangem.feature.onboarding.api.OnboardingSeedPhrase +import com.tangem.feature.onboarding.api.OnboardingSeedPhraseScreen import com.tangem.feature.onboarding.api.OnboardingSeedPhraseApi import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseViewModel @@ -15,7 +15,7 @@ import com.tangem.wallet.R [REDACTED_AUTHOR] */ internal class OnboardingSeedPhraseStateHandler( - private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhrase(), + private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhraseScreen(), ) { fun newState( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt new file mode 100644 index 0000000000..bc213e1d41 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs + +import android.app.Dialog +import android.content.Context +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.tangem.tap.common.extensions.dispatchDialogHide +import com.tangem.tap.common.feedback.SupportInfo +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.onboarding.OnboardingDialog +import com.tangem.tap.store +import com.tangem.wallet.R + +object WalletActivationErrorDialog { + + fun create(context: Context, dialog: OnboardingDialog.WalletActivationError): Dialog { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { + setTitle(context.getString(R.string.onboarding_activation_error_title)) + setMessage(context.getString(R.string.onboarding_activation_error_message)) + setPositiveButton(R.string.common_ok) { _, _ -> dialog.onConfirm() } + setNegativeButton(R.string.chat_button_title) { _, _ -> + store.dispatch(GlobalAction.OpenChat(SupportInfo())) + } + setOnDismissListener { store.dispatchDialogHide() } + setCancelable(false) + }.create() + } +} \ No newline at end of file diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 460c4ae43b..e257132463 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -308,6 +308,8 @@ Повторно введите код доступа Код доступа должен состоять не менее чем из 4 символов. Введенные коды доступа не совпадают + Необходимо повторить операцию, при этом карта будет сброшена к заводским настройкам + Ошибка активации Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить? Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас. Добавить резервную карту diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 75ab3f3f04..dd42088e6b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -311,6 +311,8 @@ Re-enter your Access Code Access code must be at least 4 characters long Entered access code didn\'t match the initial access code + Please repeat the operation. The card will be reset to factory settings. + Activation error You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process? The backup process is partly complete. You can\'t exit it now. Add a backup card diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt similarity index 97% rename from features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt rename to features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt index 75e0acda9f..978f1ea01e 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt @@ -14,7 +14,7 @@ import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseSc /** [REDACTED_AUTHOR] */ -class OnboardingSeedPhrase : OnboardingSeedPhraseApi { +class OnboardingSeedPhraseScreen : OnboardingSeedPhraseApi { @Composable override fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float) { From 443560a5fd0e1f9ab98d3a31e2d004e1aaf98181 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Jan 2024 20:49:55 +0100 Subject: [PATCH 26/28] Updated on 2026-08-14 --- .../presentation/models/AddCustomTokenViewsModels.kt | 11 ++++++++--- .../impl/presentation/ui/AddCustomTokenPreviewData.kt | 11 +++++++++-- .../ui/components/AddCustomTokenFloatingButton.kt | 5 +++-- .../viewmodels/AddCustomTokenViewModel.kt | 10 +++++++++- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt index 965525c83e..ca7f910b83 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt @@ -313,7 +313,12 @@ internal sealed class AddCustomTokenWarning(val description: TextReference) { /** * Floating button of add custom token screen * - * @property isEnabled button availability - * @property onClick lambda be invoked when button is been pressed + * @property isEnabled button availability + * @property showProgress whether circle progress indication is enabled + * @property onClick lambda be invoked when button is been pressed */ -internal data class AddCustomTokenFloatingButton(val isEnabled: Boolean, val onClick: () -> Unit) \ No newline at end of file +internal data class AddCustomTokenFloatingButton( + val isEnabled: Boolean, + val showProgress: Boolean, + val onClick: () -> Unit +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt index b5274e6d12..7283e0ccf9 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt @@ -91,7 +91,11 @@ internal object AddCustomTokenPreviewData { ), form = createDefaultForm(), warnings = createWarnings(), - floatingButton = AddCustomTokenFloatingButton(isEnabled = false, onClick = {}), + floatingButton = AddCustomTokenFloatingButton( + isEnabled = false, + showProgress = false, + onClick = {} + ), testBlock = AddCustomTokenTestBlock( chooseTokenButtonText = "Choose token", clearButtonText = "Clear address", @@ -112,7 +116,10 @@ internal object AddCustomTokenPreviewData { ), form = createDefaultForm(), warnings = createWarnings(), - floatingButton = AddCustomTokenFloatingButton(isEnabled = false, onClick = {}), + floatingButton = AddCustomTokenFloatingButton( + isEnabled = false, + showProgress = false, + onClick = {}), ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt index 558534f369..a55ee2f29e 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt @@ -32,6 +32,7 @@ internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton, m text = stringResource(id = R.string.custom_token_add_token), iconResId = R.drawable.ic_plus_24, enabled = model.isEnabled, + showProgress = model.showProgress, onClick = model.onClick, ) } @@ -48,7 +49,7 @@ private fun Preview_AddCustomTokenFloatingButton( private class AddCustomTokenFloatingButtonProvider : CollectionPreviewParameterProvider( listOf( - AddCustomTokenFloatingButton(isEnabled = true, onClick = {}), - AddCustomTokenFloatingButton(isEnabled = false, onClick = {}), + AddCustomTokenFloatingButton(isEnabled = true, showProgress = false, onClick = {}), + AddCustomTokenFloatingButton(isEnabled = false, showProgress = false, onClick = {}), ), ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index aedff94b31..f92b119763 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -142,7 +142,10 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun createFloatingButton(): AddCustomTokenFloatingButton { - return AddCustomTokenFloatingButton(isEnabled = false, onClick = actionsHandler::onAddCustomTokenClick) + return AddCustomTokenFloatingButton( + isEnabled = true, + showProgress = false, + onClick = actionsHandler::onAddCustomTokenClick) } private inner class FormStateBuilder { @@ -852,9 +855,14 @@ internal class AddCustomTokenViewModel @Inject constructor( analyticsSender.sendWhenAddTokenButtonClicked(currency) viewModelScope.launch(dispatchers.io) { + val oldButtonState = uiState.floatingButton + uiState = uiState.copySealed( + floatingButton = uiState.floatingButton.copy(isEnabled = false, showProgress = true) + ) runCatching { featureInteractor.saveToken(currency) } .onSuccess { featureRouter.openWalletScreen() } .onFailure { + uiState = uiState.copySealed(floatingButton = oldButtonState) Timber.e(it) } } From ad0c793b0012e908ba92192fb5a71f06093470db Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jan 2024 11:10:01 +0100 Subject: [PATCH 27/28] Updated on 2026-08-14 --- .../impl/presentation/models/AddCustomTokenViewsModels.kt | 2 +- .../impl/presentation/ui/AddCustomTokenPreviewData.kt | 5 +++-- .../impl/presentation/viewmodels/AddCustomTokenViewModel.kt | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt index ca7f910b83..16fba8e3f2 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt @@ -320,5 +320,5 @@ internal sealed class AddCustomTokenWarning(val description: TextReference) { internal data class AddCustomTokenFloatingButton( val isEnabled: Boolean, val showProgress: Boolean, - val onClick: () -> Unit + val onClick: () -> Unit, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt index 7283e0ccf9..ba7d22727c 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt @@ -94,7 +94,7 @@ internal object AddCustomTokenPreviewData { floatingButton = AddCustomTokenFloatingButton( isEnabled = false, showProgress = false, - onClick = {} + onClick = {}, ), testBlock = AddCustomTokenTestBlock( chooseTokenButtonText = "Choose token", @@ -119,7 +119,8 @@ internal object AddCustomTokenPreviewData { floatingButton = AddCustomTokenFloatingButton( isEnabled = false, showProgress = false, - onClick = {}), + onClick = {}, + ), ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index f92b119763..fae60b48c3 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -145,7 +145,8 @@ internal class AddCustomTokenViewModel @Inject constructor( return AddCustomTokenFloatingButton( isEnabled = true, showProgress = false, - onClick = actionsHandler::onAddCustomTokenClick) + onClick = actionsHandler::onAddCustomTokenClick, + ) } private inner class FormStateBuilder { @@ -857,7 +858,7 @@ internal class AddCustomTokenViewModel @Inject constructor( viewModelScope.launch(dispatchers.io) { val oldButtonState = uiState.floatingButton uiState = uiState.copySealed( - floatingButton = uiState.floatingButton.copy(isEnabled = false, showProgress = true) + floatingButton = uiState.floatingButton.copy(isEnabled = false, showProgress = true), ) runCatching { featureInteractor.saveToken(currency) } .onSuccess { featureRouter.openWalletScreen() } From 3968f4659cd101572b9ce045450e839b5b015805 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 2 Feb 2024 15:48:08 +0300 Subject: [PATCH 28/28] Updated on 2026-08-14 --- gradle/dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index f6b9405dcf..826d5cf01f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,9 +85,9 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-471" +tangemBlockchainSdk = "release-app_5.6-472" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-324" +tangemCardSdk = "release-app_5.6-325" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem