diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index fdf0660901..07cfce37ff 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit fdf0660901442ade296db42bb867fc5c16a14262 +Subproject commit 07cfce37ff84e70081aca82d9948acb13c5c07b0 diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt index 09a32da708..f06f2f801a 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt @@ -33,7 +33,9 @@ class ResetToFactorySettingsTask( } private fun resetBackup(session: CardSession, callback: (result: CompletionResult) -> Unit) { - if (session.environment.card?.backupStatus == Card.BackupStatus.NoBackup) { + if (session.environment.card?.backupStatus == null || + session.environment.card?.backupStatus == Card.BackupStatus.NoBackup + ) { callback(CompletionResult.Success(session.environment.card!!)) return } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 23484d916d..fd6d7105d3 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -17,7 +17,6 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import arrow.core.getOrElse import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.textfield.TextInputEditText import com.tangem.Message @@ -25,14 +24,11 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.qrscanning.models.QrResult import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.features.send.api.navigation.SendRouter.Companion.CRYPTO_CURRENCY_KEY import com.tangem.sdk.extensions.hideSoftKeyboard import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.analytics.events.Token @@ -63,7 +59,6 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import java.text.DecimalFormatSymbols import javax.inject.Inject @@ -87,9 +82,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { private val sendSubscriber = SendStateSubscriber(this) private lateinit var keyboardObserver: KeyboardObserver - private val cryptoCurrency: CryptoCurrency? - get() = arguments?.getParcelable(CRYPTO_CURRENCY_KEY) - val binding: FragmentSendBinding by viewBinding(FragmentSendBinding::bind) @Inject @@ -109,7 +101,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { super.onViewCreated(view, savedInstanceState) viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - subscribeToQrCodeScanner() subscribeToTransactionExtrasFields() subscribeToAddressField() subscribeToAmountField() @@ -200,22 +191,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { .launchIn(this) } - private fun CoroutineScope.subscribeToQrCodeScanner() { - listenToQrScanningUseCase(SourceType.SEND) - .getOrElse { emptyFlow() } - .onEach { rawQr -> - cryptoCurrency?.let { cryptoCurrency -> - parseQrCodeUseCase(rawQr, cryptoCurrency = cryptoCurrency).fold( - ifLeft = { - onCodeScanned(QrResult(address = rawQr)) - Timber.w(it) - }, - ifRight = { onCodeScanned(it) }, - ) - } ?: onCodeScanned(QrResult(address = rawQr)) - }.launchIn(this) - } - private fun CoroutineScope.subscribeToTransactionExtrasFields() = with(binding.lSendAddress) { // TODO: [REDACTED_TASK_KEY] etXlmMemo.inputtedTextAsFlow() @@ -282,21 +257,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { .launchIn(this@subscribeToTransactionExtrasFields) } - private fun onCodeScanned(parsedQr: QrResult) { - if (parsedQr.address.isEmpty()) return - - store.dispatch( - PasteAddress( - data = parsedQr.address, - sourceType = Token.Send.AddressEntered.SourceType.QRCode, - ), - ) - parsedQr.amount?.let { amount -> - store.dispatchOnMain(AmountAction.SetAmount(amount, isUserInput = false)) - } - store.dispatch(TruncateOrRestore(!binding.lSendAddress.etAddress.isFocused)) - } - private fun setupAmountLayout() { store.dispatch(SetMainCurrency(restoreMainCurrency())) store.dispatch(ReceiptAction.RefreshReceipt) diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt index de5e810b88..9f59533686 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt @@ -1,7 +1,11 @@ package com.tangem.tap.features.send.ui import androidx.lifecycle.* +import arrow.core.getOrElse import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.FetchPendingTransactionsUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency @@ -9,12 +13,16 @@ import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.di.DelayedWork +import com.tangem.tap.features.send.redux.AddressActionUi import com.tangem.tap.features.send.redux.AmountAction import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -31,10 +39,19 @@ internal class SendViewModel @Inject constructor( private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, + private val listenToQrScanningUseCase: ListenToQrScanningUseCase, + private val parseQrCodeUseCase: ParseQrCodeUseCase, @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { + init { + listenToQrScanningUseCase(SourceType.SEND) + .getOrElse { emptyFlow() } + .onEach(::onQRCodeScanned) + .launchIn(viewModelScope) + } + private val cryptoCurrency: CryptoCurrency? = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY] override fun onCreate(owner: LifecycleOwner) { @@ -79,6 +96,43 @@ internal class SendViewModel @Inject constructor( ) } + private fun onQRCodeScanned(qrScanResult: String) { + if (cryptoCurrency != null) { + parseQrCodeUseCase(qrScanResult, cryptoCurrency).fold( + ifRight = { parsedCode -> + store.dispatch( + AddressActionUi.PasteAddress( + data = parsedCode.address, + sourceType = Token.Send.AddressEntered.SourceType.QRCode, + ), + ) + parsedCode.amount?.let { amount -> + store.dispatch(AmountAction.SetAmount(amount, isUserInput = false)) + } + // parsedCode.memo?.let { } + }, + ifLeft = { + store.dispatch( + AddressActionUi.PasteAddress( + data = qrScanResult, + sourceType = Token.Send.AddressEntered.SourceType.QRCode, + ), + ) + Timber.w(it) + }, + ) + } else { + store.dispatch( + AddressActionUi.PasteAddress( + data = qrScanResult, + sourceType = Token.Send.AddressEntered.SourceType.QRCode, + ), + ) + } + + store.dispatch(AddressActionUi.TruncateOrRestore(truncate = true)) + } + companion object { private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L private const val TAG = "SendViewModel" diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt index bfd30da35b..dcacf9fef2 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.tokens.impl.data import androidx.paging.PagingSource import androidx.paging.PagingState import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -21,16 +22,25 @@ import com.tangem.utils.coroutines.runCatching * @property dispatchers coroutine dispatchers provider * @property getSelectedWalletSyncUseCase use case that returns selected wallet * @property searchText search text + * @property needFilterExcluded filter networks that are not supported in the app */ internal class TangemApiTokensPagingSource( private val api: TangemTechApi, private val dispatchers: CoroutineDispatcherProvider, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val searchText: String?, - needFilterExcluded: Boolean = false, + private val needFilterExcluded: Boolean = false, ) : PagingSource() { private val coinsResponseConverter = CoinsResponseConverter(needFilterExcluded) + + private val allAvailableBlockchains by lazy { + Blockchain.entries + .filter { + it.isTestnet().not() && (needFilterExcluded.not() || it.isSupportedInApp()) + } + } + override fun getRefreshKey(state: PagingState): Int? { return state.anchorPosition?.let { anchorPosition -> state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1) @@ -43,7 +53,7 @@ internal class TangemApiTokensPagingSource( return runCatching(dispatchers.io) { val supportedBlockchains = getSelectedWalletSyncUseCase().fold( - ifLeft = { Blockchain.entries }, + ifLeft = { allAvailableBlockchains }, ifRight = { it.scanResponse.card.supportedBlockchains(it.scanResponse.cardTypesResolver) }, ) 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 aa15f116e8..39a0168df7 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 @@ -223,6 +223,10 @@ object TradeCryptoMiddleware { } private fun handleSendCoin(action: TradeCryptoAction.SendCoin) { + if (action.transactionInfo?.tag != null) { + // avoid open old send if memo exists + return + } val cryptoStatus = action.coinStatus val currency = cryptoStatus.currency val blockchain = Blockchain.fromId(currency.network.id.value) diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt index 3c12cf80a8..790de00ea3 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt @@ -4,6 +4,8 @@ import com.tangem.core.analytics.api.* import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import java.util.concurrent.Executors /** @@ -22,6 +24,7 @@ object Analytics : GlobalAnalyticsEventHandler { private val handlers = mutableMapOf() private val paramsInterceptors = mutableMapOf() private val analyticsFilters = mutableSetOf() + private val analyticsMutex = Mutex() private val analyticsHandlers: List get() = handlers.values.toList() @@ -55,23 +58,26 @@ object Analytics : GlobalAnalyticsEventHandler { event.params = applyParamsInterceptors(event) val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) } - when { - eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(event) } - eventFilter.canBeSent(event) -> { - analyticsHandlers - .filter { handler -> eventFilter.canBeConsumedByHandler(handler, event) } - .forEach { handler -> handler.send(event) } + analyticsMutex.withLock { + when { + eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(event) } + eventFilter.canBeSent(event) -> { + analyticsHandlers + .filter { handler -> eventFilter.canBeConsumedByHandler(handler, event) } + .forEach { handler -> handler.send(event) } + } } } } } - private fun applyParamsInterceptors(event: AnalyticsEvent): MutableMap { + private suspend fun applyParamsInterceptors(event: AnalyticsEvent): MutableMap { val interceptedParams = event.params.toMutableMap() - paramsInterceptors.values - .filter { it.canBeAppliedTo(event) } - .forEach { it.intercept(interceptedParams) } - + analyticsMutex.withLock { + paramsInterceptors.values + .filter { it.canBeAppliedTo(event) } + .forEach { it.intercept(interceptedParams) } + } return interceptedParams } diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index b3f4ebb779..7de6a6f134 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -13,7 +13,7 @@ }, { "name": "LOCAL_USER_LOGS_ENABLED", - "version": "5.8.0" + "version": "5.10.0" }, { "name": "GENERATE_XPUB_ENABLED", diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index adb56faef9..6e9e7bf51e 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -4,12 +4,16 @@ Валюты Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. Обратиться в поддержку + Попробовать снова Эта функция недоступна в демонстрационном режиме Причина: %s Не могу отправить транзакцию Выбранный кошелёк не поддерживает сеть %1$s Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен. Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки. + Спасибо за ваш отзыв. Мы ответим в кратчайшие сроки. + Ваши предложения отправлены + Пожалуйста, попробуйте приложить карту в точности, как показано на анимации, или запросите поддержку. У вас возникли трудности со сканированием карты? Эта карта не предназначена для работы с этим приложением Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться. @@ -24,6 +28,8 @@ Тёмная Светлая Как в системе + При выборе настройки как в системе приложение будет использовать тему в соответствии с настройками вашего устройства + Системная Тема Настройки приложения Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\" @@ -75,6 +81,7 @@ Создать Удалить Отключено + Отключить Готово Включить Включено @@ -107,6 +114,7 @@ Отклонить Перезагрузить Переименовать + Повторить Сохранить изменения Искать Поиск токенов @@ -130,6 +138,7 @@ Я понял Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно + Предупреждение Да Адрес контракта скопирован! Доступные сети @@ -176,6 +185,7 @@ Скрывать балансы жестом переворота Эмитент Подписано + Если вы забудете код, то потеряете доступ к своим средствам. Восстановление кода невозможно. Подробности Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования @@ -189,7 +199,7 @@ Выберите провайдера Произошла ошибка. Код: %s К сожалению, обмен указанной пары через выбранного провайдера на данный момент невозможен. Попробуйте совершить обмен позже. (Код: %s) - Выбранный провайдер не доступен для обмена. Попробуйте позже. (Код: %s) + Выбранный провайдер недоступен для обмена. Попробуйте позже. (Код: %s) В данный момент обмен невозможен. Попробуйте позже. (Код: %s) Курс обмена Обмен через %s @@ -460,16 +470,17 @@ Комиссия не превысит Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение. Допустим ввод только цифр - Сумма отправки будет уменьшена на %1$s для покрытия выбранного уровня комиссии. Получателю будет отправлено %2$s. + Сумма отправки будет уменьшена на %1$s (%2$s) для покрытия выбранного уровня комиссии Покрытие сетевой комиссии Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса Недостаточно средств - Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, убедитесь, что остаток после отправки будет не менее %s. + Оставить %s + Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, оставьте %s на балансе. Экзистенциальный депозит Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Установлена высокая комиссия - Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s. - Комиссия увеличилась + Ввиду особенности сети Tezos комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s. + Комиссия повышена Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению Недопустимая сумма Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %2$s. @@ -490,7 +501,8 @@ Отправить Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. Мои кошельки - Это способ измерения комиссии за отправку биткоин-транзакции. Он указывает на количество самой маленькой единицы биткоина (сатоши) за каждый байт данных в транзакции. Чем выше это число, тем быстрее будет обработана транзакция сетью. + Способ измерения комиссии за биткоин-транзакцию. Он указывает на количество самой маленькой единицы биткоина (сатоши) за каждый виртуальный байт в транзакции. Чем выше число, тем быстрее будет обработана транзакция майнерами. + Сатоши / вбайт Отправка Нажмите на любое поле, чтобы изменить его Отправка %s @@ -547,6 +559,7 @@ В данный момент обмен монеты %s недоступен. Следите за нашими обновлениями. Продажа средств станет доступной после завершения транзакции %s В данный момент продажа %s недоступна. Следите за нашими обновлениями. + Выберите адрес Сгенерировать XPUB Скрыть Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index f40c8e0daa..2fb4ea6a93 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -4,12 +4,16 @@ Manage tokens Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. Request support + Try again This feature is disabled in Demo mode Reason: %s Can\'t send a transaction The selected does not support the %1$s network To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset. Tokens in %1$s network are not supported by this card due to firmware limitation. + Thank you for your feedback. We will respond as soon as possible + Your suggestions were sent + Please try to tap the card exactly as shown in the animation or request support. Are you having difficulty scanning your card? This card is not designed to work with this app Default Fee @@ -25,6 +29,8 @@ Dark Light System default + If system is selected, the app will auto-adjust based on your device\'s system settings + System Theme App Settings To hide or show your balances, simply flip your device screen down, or switch it off in Settings @@ -74,6 +80,7 @@ Create Delete Disabled + Disconnect Done Enable Enabled @@ -106,6 +113,7 @@ Reject Reload Rename + Retry Save changes Search Search tokens @@ -129,6 +137,7 @@ I understand There was an error. Please try again. Unreachable + Warning Yes Contract address copied! Available networks @@ -175,6 +184,7 @@ Flip-to-Hide Balances Issuer Signed + If you forget the code you will lose access to your funds. Code recovery is not possible. Details Check your internet connection or switch to a different network Terms of Service @@ -457,16 +467,17 @@ Max fee The fee that will be charged for your transaction. You can set your own value. Numbers only for Destination Tag - Sending amount will be reduced by %1$s to cover the selected commission level. The recipient will get %2$s. + Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level 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 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. + Leave %s + The account will be wiped from the blockchain if a balance goes below the existential deposit. Please leave %s on your balance. Existential deposit The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. Custom fee is high - The fee for transferring the entire balance is higher. To reduce the commission, you can leave %s. - Fee is increased + Due to the peculiarities of the Tezos network, the fee for transferring the entire balance is higher. To reduce the commission, you can leave %s. + The fee is higher The included commission exceeds the transfer amount, leading to a negative value Invalid amount The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %2$s. @@ -489,8 +500,8 @@ Send to A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds My wallets - The fee for a Bitcoin transaction is measured by the number of the smallest Bitcoin unit (Satoshi) per byte of data. The higher this number, the faster the transaction will be processed. - Satoshi per vbyte + A way of measuring Bitcoin transaction fees. It indicates the number of the smallest Bitcoin unit (Satoshi) for each virtual byte in a transaction. The higher the number, the faster the transaction will be processed by miners. + Satoshi / vByte Sending... Tap any field to change it Send %s @@ -548,6 +559,7 @@ Swapping %s is not available at the moment. Please check our updates. Selling funds will be available once the %s transaction is complete Selling %s is not available at the moment. Please check our updates. + Choose address Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt index 09b6158abb..c65bf02622 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt @@ -63,7 +63,8 @@ fun AppBarWithBackButtonAndIconContent( ) Column( verticalArrangement = Arrangement.Center, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) + .animateContentSize(), ) { AnimatedVisibility( visible = !text.isNullOrBlank(), @@ -89,7 +90,6 @@ fun AppBarWithBackButtonAndIconContent( color = TangemTheme.colors.text.secondary, maxLines = 1, style = TangemTheme.typography.caption2, - modifier = Modifier.animateContentSize(), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt index f51adb19b3..0f7ff350de 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt @@ -60,24 +60,23 @@ sealed class TokenIconState { * @property background The background color to be used for the icon. * @property networkBadgeIconResId The drawable resource ID for the network badge. * @property isGrayscale Specifies whether to show the icon in grayscale. + * @property showCustomBadge Specifies whether to show the custom token badge. */ data class CustomTokenIcon( val tint: Color, val background: Color, @DrawableRes override val networkBadgeIconResId: Int, override val isGrayscale: Boolean, - ) : TokenIconState() { + override val showCustomBadge: Boolean = true, + ) : TokenIconState() - override val showCustomBadge: Boolean = true - } - - object Loading : TokenIconState() { + data object Loading : TokenIconState() { override val isGrayscale: Boolean = false override val showCustomBadge: Boolean = false override val networkBadgeIconResId: Int? = null } - object Locked : TokenIconState() { + data object Locked : TokenIconState() { override val isGrayscale: Boolean = false override val showCustomBadge: Boolean = false override val networkBadgeIconResId: Int? = null diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt index fbcc6e3caa..3e62e98992 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt @@ -30,6 +30,7 @@ class CryptoCurrencyToIconStateConverter : Converter getIconStateForToken( token = currency, @@ -50,13 +51,14 @@ class CryptoCurrencyToIconStateConverter : Converter Unit = {}, @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false) reduceFactor: Double = 0.9, ) { @@ -101,6 +103,8 @@ fun AmountTextField( fontSize = fontSize, textDirection = TextDirection.ContentOrLtr, ), + isValuePasted = isValuePasted, + onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, color = textColor, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, 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 b9cba56550..ff2bf968ae 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 @@ -39,13 +39,16 @@ fun SimpleTextField( color: Color = TangemTheme.colors.text.primary1, textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color), readOnly: Boolean = false, + isValuePasted: Boolean = false, + onValuePastedTriggerDismiss: () -> Unit = {}, decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null, ) { + val proxyValue by remember(value) { derivedStateOf { value } } var textFieldValueState by remember { mutableStateOf( TextFieldValue( text = value, - selection = getValueRange(value), + selection = TextRange(value.length, value.length), ), ) } @@ -54,32 +57,31 @@ fun SimpleTextField( handleColor = TangemTheme.colors.text.accent, backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f), ) - val textFieldValue = textFieldValueState.copy(text = value) - - val isSelectionChanged by remember { - derivedStateOf { - textFieldValue.selection != textFieldValueState.selection || - textFieldValue.composition != textFieldValueState.composition || - textFieldValue.text != textFieldValueState.text - } + var lastTextValue by remember(proxyValue, isValuePasted) { + textFieldValueState = textFieldValueState.copy( + text = proxyValue, + selection = if (isValuePasted) { + TextRange(proxyValue.length, proxyValue.length) + } else { + textFieldValueState.selection + }, + ) + mutableStateOf(proxyValue) } + val isSelectionChanged by rememberSelectionChanged(textFieldValue, textFieldValueState) LaunchedEffect(key1 = isSelectionChanged) { if (isSelectionChanged) { textFieldValueState = textFieldValue } } - var lastTextValue by remember(value) { - val isSelectionLastIndex = textFieldValueState.selection.end == textFieldValueState.text.lastIndex - if (textFieldValueState.text.isBlank() || isSelectionLastIndex) { - textFieldValueState = textFieldValueState.copy( - text = value, - selection = getValueRange(value), - ) + // resets paste value cursor trigger + LaunchedEffect(key1 = isValuePasted) { + if (isValuePasted) { + onValuePastedTriggerDismiss() } - mutableStateOf(value) } CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) { @@ -114,11 +116,6 @@ fun SimpleTextField( } } -private fun getValueRange(value: String) = when { - value.isEmpty() -> TextRange.Zero - else -> TextRange(value.length, value.length) -} - @Composable private fun SimpleTextPlaceholder( placeholder: TextReference?, @@ -141,4 +138,14 @@ private fun SimpleTextPlaceholder( } textValue() } +} + +@Composable +private fun rememberSelectionChanged(textFieldValue: TextFieldValue, textFieldValueState: TextFieldValue) = remember { + derivedStateOf { + val isSelectionChanged = textFieldValue.selection != textFieldValueState.selection || + textFieldValue.composition != textFieldValueState.composition + val isTextNotChanged = textFieldValue.text == textFieldValueState.text + isSelectionChanged && isTextNotChanged + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt index c16d459a12..41bbc546b7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment.Companion.CenterEnd import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier @@ -23,6 +23,7 @@ import com.tangem.core.ui.components.inputrow.inner.PasteButton import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import kotlinx.coroutines.delay /** * [Input Row Recipient](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-826&mode=design&t=IQ5lBJEkFGU4WSvi-4) @@ -115,8 +116,18 @@ fun InputRowRecipient( @Composable private fun RowScope.InputIcon(isLoading: Boolean, value: String) { + var isLoadingProxy by remember { mutableStateOf(isLoading) } + + // Do not show the progress indicator, which will disappear quickly + LaunchedEffect(key1 = isLoading) { + if (isLoading) { + delay(timeMillis = 500) + } + isLoadingProxy = isLoading + } + AnimatedContent( - targetState = isLoading, + targetState = isLoadingProxy, label = "Indicator Show Change", modifier = Modifier .align(CenterVertically) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt index bedec66060..b919363574 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt @@ -73,7 +73,7 @@ fun SelectorRowItem( color = TangemTheme.colors.text.primary1, modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), ) - if (preDot != null && postDot != null) { + if (preDot != null) { SelectorValueContent( preDot = preDot, postDot = postDot, @@ -97,7 +97,7 @@ fun SelectorRowItem( @Composable private fun RowScope.SelectorValueContent( preDot: TextReference, - postDot: TextReference, + postDot: TextReference?, ellipsizeOffset: Int? = null, ) { val ellipsis = if (ellipsizeOffset == null) { @@ -115,18 +115,20 @@ private fun RowScope.SelectorValueContent( .weight(1f) .padding(start = TangemTheme.dimens.spacing4), ) - Text( - text = "•", - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing4), - ) - Text( - text = postDot.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) + if (postDot != null) { + Text( + text = "•", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing4), + ) + Text( + text = postDot.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } } @Preview 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 76b16b1409..3fe078eaad 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 @@ -71,7 +71,10 @@ internal class DefaultTransactionRepository( } Blockchain.Binance -> BinanceTransactionExtras(memo) Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) } - Blockchain.Cosmos -> CosmosTransactionExtras(memo) + Blockchain.Cosmos, + Blockchain.TerraV1, + Blockchain.TerraV2, + -> CosmosTransactionExtras(memo) Blockchain.TON -> TonTransactionExtras(memo) Blockchain.Hedera -> HederaTransactionBuilder.HederaTransactionExtras(memo) Blockchain.Algorand -> AlgorandTransactionExtras(memo) 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 eded0146fc..91430442e1 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 @@ -197,12 +197,6 @@ class GetCurrencyWarningsUseCase( coinCurrency = coinStatus.currency, ) } - feePaidCurrency is FeePaidCurrency.SameCurrency && tokenStatus.value.amount.isZero() -> { - CryptoCurrencyWarning.BalanceNotEnoughForFee( - tokenCurrency = tokenStatus.currency, - coinCurrency = coinStatus.currency, - ) - } feePaidCurrency is FeePaidCurrency.Token -> { val feePaidTokenBalance = feePaidCurrency.balance val amount = tokenStatus.value.amount ?: return null 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 3e6005ebc7..4ae1a1f01c 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 @@ -254,10 +254,18 @@ internal class CurrenciesStatusesOperations( var quotesRetrievingFailed = false val networksStatuses = maybeNetworkStatuses?.bind()?.toNonEmptySetOrNull() - val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) { - quotesRetrievingFailed = true - null - } + val quotes: Set? = maybeQuotes?.fold( + ifLeft = { + quotesRetrievingFailed = true + null + }, + ifRight = { + it.ifEmpty { + quotesRetrievingFailed = true + null + } + }, + ) currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 64e8f1e992..da17befee3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -4,27 +4,18 @@ import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.flowWithLifecycle -import androidx.lifecycle.lifecycleScope -import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder -import com.tangem.domain.qrscanning.models.SourceType -import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.ui.SendScreen import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.launch import java.lang.ref.WeakReference import javax.inject.Inject @@ -40,9 +31,6 @@ internal class SendFragment : ComposeFragment() { @Inject lateinit var router: SendRouter - @Inject - lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase - @Inject lateinit var analyticsEventsHandler: AnalyticsEventHandler @@ -65,7 +53,6 @@ internal class SendFragment : ComposeFragment() { analyticsEventsHandler = analyticsEventsHandler, ), ) - listenToQrCode() } @Composable @@ -83,25 +70,7 @@ internal class SendFragment : ComposeFragment() { super.onDestroy() } - private fun listenToQrCode() { - lifecycleScope.launch { - listenToQrScanningUseCase(SourceType.SEND) - .getOrElse { emptyFlow() } - .flowWithLifecycle(this@SendFragment.lifecycle, minActiveState = Lifecycle.State.CREATED) - .collect { - delay(QR_SCAN_DELAY) - - // Delayed launch is needed in order for the UI to be drawn and to process the sent events. - // If do not use the delay, then error field is not displayed when - // inserting an incorrect amount by shareUri - viewModel.onQrCodeScanned(it) - } - } - } - companion object { - private const val QR_SCAN_DELAY = 200L - /** Create send fragment instance */ fun create(): SendFragment = SendFragment() } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt index e99471d8de..feca8a0786 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt @@ -16,6 +16,20 @@ internal sealed class SendAnalyticEvents( params: Map = mapOf(), ) : AnalyticsEvent(category = "Token / Send", event = event, params = params) { + /** Close button clicked */ + data class CloseButtonClicked( + val source: SendScreenSource, + val isFromSummary: Boolean, + val isValid: Boolean, + ) : SendAnalyticEvents( + event = "Button - Close", + params = mapOf( + SOURCE to source.name, + "FromSummary" to if (isFromSummary) "Yes" else "No", + "isValid" to if (isValid) "Yes" else "No", + ), + ) + // region Address /** Recipient address screen opened */ data object AddressScreenOpened : SendAnalyticEvents(event = "Address Screen Opened") @@ -119,6 +133,7 @@ internal enum class SendScreenSource { Address, Amount, Fee, + Confirm, } internal enum class EnterAddressSource { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendOnNextScreenAnalyticSender.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt similarity index 60% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendOnNextScreenAnalyticSender.kt rename to features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt index 6ca1759174..27c93b9743 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendOnNextScreenAnalyticSender.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt @@ -5,18 +5,23 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.features.send.impl.presentation.analytics.SelectedCurrencyType import com.tangem.features.send.impl.presentation.analytics.SelectedFeeType import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents +import com.tangem.features.send.impl.presentation.analytics.SendScreenSource 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.StateRouter import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.utils.Provider -internal class SendOnNextScreenAnalyticSender( +internal class SendScreenAnalyticSender( + private val stateRouterProvider: Provider, + private val currentStateProvider: Provider, private val analyticsEventHandler: AnalyticsEventHandler, ) { fun send(prevScreen: SendUiStateType, state: SendUiState) { when (prevScreen) { SendUiStateType.Fee -> { - val feeState = state.feeState ?: return + val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content feeSelectorState?.selectedFee?.let { selectedFee -> val isCustomFeeEdited = feeState.fee?.amount?.value != feeSelectorState.fees.normal.amount.value @@ -27,7 +32,8 @@ internal class SendOnNextScreenAnalyticSender( } } SendUiStateType.Amount -> { - val isFiatSelected = state.amountState?.amountTextField?.isFiatValue ?: return + val amountState = state.getAmountState(stateRouterProvider().isEditState) ?: return + val isFiatSelected = amountState.amountTextField.isFiatValue val selectedCurrency = if (!isFiatSelected) { SelectedCurrencyType.Token } else { @@ -41,6 +47,32 @@ internal class SendOnNextScreenAnalyticSender( } } + fun sendOnClose() { + val routerState = stateRouterProvider().currentState.value + val state = currentStateProvider() + + val (source, isValid) = when (routerState.type) { + SendUiStateType.Recipient, + SendUiStateType.EditRecipient, + -> SendScreenSource.Address to (state.editRecipientState?.isPrimaryButtonEnabled ?: false) + SendUiStateType.Amount, + SendUiStateType.EditAmount, + -> SendScreenSource.Amount to (state.editAmountState?.isPrimaryButtonEnabled ?: false) + SendUiStateType.Fee, + SendUiStateType.EditFee, + -> SendScreenSource.Fee to (state.editFeeState?.isPrimaryButtonEnabled ?: false) + else -> SendScreenSource.Confirm to true + } + + analyticsEventHandler.send( + SendAnalyticEvents.CloseButtonClicked( + source = source, + isFromSummary = routerState.isFromConfirmation, + isValid = isValid, + ), + ) + } + private fun sendSelectedFeeAnalytics(feeSelectorState: FeeSelectorState.Content) { val type = when (feeSelectorState.fees) { is TransactionFee.Single -> SelectedFeeType.Fixed 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 616fad69f8..852339a7ed 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 @@ -20,6 +20,7 @@ import java.math.BigDecimal * @param feeStateFactory [FeeStateFactory] */ internal class SendEventStateFactory( + private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, private val clickIntents: SendClickIntents, @@ -50,7 +51,8 @@ internal class SendEventStateFactory( fun getFeeUpdatedAlert(fee: TransactionFee, onConsume: () -> Unit, onFeeNotIncreased: () -> Unit): SendUiState { val state = currentStateProvider() - val feeSelector = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state + val feeState = state.getFeeState(stateRouterProvider().isEditState) + val feeSelector = feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state val newFee = when (fee) { is TransactionFee.Single -> fee.normal is TransactionFee.Choosable -> { 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 3334f3faaa..a22b3ee620 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 @@ -94,9 +94,13 @@ internal sealed class SendNotification(val config: NotificationConfig) { }, ) - data class ExistentialDeposit(val deposit: String) : Error( + data class ExistentialDeposit(val deposit: String, val onConfirmClick: () -> Unit) : Error( title = resourceReference(R.string.send_notification_existential_deposit_title), subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_existential_deposit_button, wrappedList(deposit)), + onClick = onConfirmClick, + ), ) } @@ -149,9 +153,12 @@ internal sealed class SendNotification(val config: NotificationConfig) { ), ) - data object FeeCoverageNotification : Warning( + data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning( title = resourceReference(R.string.send_network_fee_warning_title), - subtitle = resourceReference(R.string.swapping_network_fee_warning_content), + subtitle = resourceReference( + R.string.send_network_fee_warning_content, + wrappedList(cryptoAmount, fiatAmount), + ), ) } } \ 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 b31e9559be..631cbdf100 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,8 +14,11 @@ 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.common.SendSyncEditConverter import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter +import com.tangem.features.send.impl.presentation.state.fee.checkFeeCoverage import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientHistoryListConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter @@ -26,10 +29,12 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import timber.log.Timber +import java.math.BigDecimal -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class SendStateFactory( private val clickIntents: SendClickIntents, + private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val userWalletProvider: Provider, private val appCurrencyProvider: Provider, @@ -43,6 +48,7 @@ internal class SendStateFactory( private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { SendAmountFieldConverter( clickIntents = clickIntents, + stateRouterProvider = stateRouterProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, appCurrencyProvider = appCurrencyProvider, ) @@ -81,7 +87,9 @@ internal class SendStateFactory( cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } - + private val sendSyncEditConverter by lazy(LazyThreadSafetyMode.NONE) { + SendSyncEditConverter(currentStateProvider = currentStateProvider) + } // region UI states fun getInitialState(): SendUiState = SendUiState( clickIntents = clickIntents, @@ -89,6 +97,7 @@ internal class SendStateFactory( isEditingDisabled = false, isBalanceHidden = false, cryptoCurrencyName = "", + isSubtracted = false, ) fun getReadyState(): SendUiState { @@ -116,6 +125,8 @@ internal class SendStateFactory( ) } + fun syncEditStates(isFromEdit: Boolean) = sendSyncEditConverter.convert(isFromEdit) + fun getOnHideBalanceState(isBalanceHidden: Boolean): SendUiState { return currentStateProvider().copy(isBalanceHidden = isBalanceHidden) } @@ -142,8 +153,10 @@ internal class SendStateFactory( fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false): SendUiState { val state = currentStateProvider() - val recipientState = state.recipientState ?: return state - return state.copy( + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, recipientState = recipientState.copy( addressTextField = recipientState.addressTextField.copy(value = value), memoTextField = recipientState.memoTextField?.copy(isEnabled = !isXAddress), @@ -154,7 +167,8 @@ internal class SendStateFactory( fun getOnRecipientAddressValidState(value: String, isValidAddress: Boolean): SendUiState { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val state = currentStateProvider() - val recipientState = state.recipientState ?: return state + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state val isValidMemo = validateWalletMemoUseCase( memo = recipientState.memoTextField?.value.orEmpty(), @@ -166,7 +180,8 @@ internal class SendStateFactory( val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses ?.any { it.value == value } ?: true - return state.copy( + return state.copyWrapped( + isEditState = isEditState, recipientState = recipientState.copy( isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet, isValidating = false, @@ -184,16 +199,20 @@ internal class SendStateFactory( fun getOnRecipientAddressValidationStarted(): SendUiState { val state = currentStateProvider() - val recipientState = state.recipientState ?: return state - return state.copy( + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, recipientState = recipientState.copy(isValidating = true), ) } fun getOnRecipientMemoValueChange(value: String): SendUiState { val state = currentStateProvider() - val recipientState = state.recipientState ?: return state - return state.copy( + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, recipientState = recipientState.copy( memoTextField = recipientState.memoTextField?.copy(value = value), ), @@ -203,7 +222,8 @@ internal class SendStateFactory( fun getOnRecipientMemoValidState(value: String, isValidAddress: Boolean): SendUiState { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val state = currentStateProvider() - val recipientState = state.recipientState ?: return state + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state val isValidMemo = validateWalletMemoUseCase( memo = value, @@ -215,7 +235,8 @@ internal class SendStateFactory( val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses ?.any { it.value == value } ?: true - return state.copy( + return state.copyWrapped( + isEditState = isEditState, recipientState = recipientState.copy( isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet, isValidating = false, @@ -229,8 +250,10 @@ internal class SendStateFactory( fun getOnXAddressMemoState(): SendUiState { val state = currentStateProvider() - val recipientState = state.recipientState ?: return state - return state.copy( + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, recipientState = recipientState.copy( memoTextField = recipientState.memoTextField?.copy( value = "", @@ -242,9 +265,11 @@ internal class SendStateFactory( fun getHiddenRecentListState(isAddressInWallet: Boolean, isValidAddress: Boolean): SendUiState { val state = currentStateProvider() - val recipientState = state.recipientState ?: return state + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state val isNotValid = isAddressInWallet || !isValidAddress - return state.copy( + return state.copyWrapped( + isEditState = isEditState, recipientState = recipientState.copy( recent = recipientState.recent.map { recent -> recent.copy(isVisible = isNotValid && (recent.isLoading || recent.title != TextReference.EMPTY)) @@ -258,12 +283,33 @@ internal class SendStateFactory( //endregion //region send + fun getIsAmountSubtractedState(isAmountSubtractAvailable: Boolean): SendUiState { + val state = currentStateProvider() + val balance = cryptoCurrencyStatusProvider().value.amount ?: return state + val amountState = state.getAmountState(stateRouterProvider().isEditState) ?: return state + val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return state + val amountValue = amountState.amountTextField.cryptoAmount.value ?: return state + val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO + return state.copy( + isSubtracted = checkFeeCoverage( + isSubtractAvailable = isAmountSubtractAvailable, + balance = balance, + amountValue = amountValue, + feeValue = feeValue, + ), + ) + } + fun getSendingStateUpdate(isSending: Boolean): SendUiState { val state = currentStateProvider() return state.copy( sendState = state.sendState?.copy( isSending = isSending, - isPrimaryButtonEnabled = !isSending, + isPrimaryButtonEnabled = isPrimaryButtonEnabled( + state = state, + isSending = isSending, + notifications = state.sendState.notifications, + ), ), ) } @@ -285,10 +331,13 @@ internal class SendStateFactory( fun getSendNotificationState(notifications: ImmutableList): SendUiState { val state = currentStateProvider() val sendState = state.sendState ?: return state - val hasErrorNotifications = notifications.any { it is SendNotification.Error } return state.copy( sendState = sendState.copy( - isPrimaryButtonEnabled = !hasErrorNotifications, + isPrimaryButtonEnabled = isPrimaryButtonEnabled( + state = state, + isSending = sendState.isSending, + notifications = notifications, + ), notifications = notifications, showTapHelp = sendState.showTapHelp && notifications.isEmpty(), ), @@ -302,5 +351,15 @@ internal class SendStateFactory( sendState = sendState.copy(showTapHelp = false), ) } + + private fun isPrimaryButtonEnabled( + state: SendUiState, + isSending: Boolean, + notifications: ImmutableList, + ): Boolean { + val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return false + val hasErrorNotifications = notifications.any { it is SendNotification.Error } + return !hasErrorNotifications && !isSending && feeState.feeSelectorState is FeeSelectorState.Content + } //endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index 2b14283039..84adf93793 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 @@ -28,9 +28,59 @@ internal data class SendUiState( val recipientState: SendStates.RecipientState? = null, val feeState: SendStates.FeeState? = null, val sendState: SendStates.SendState? = null, + val editAmountState: SendStates.AmountState? = null, + val editRecipientState: SendStates.RecipientState? = null, + val editFeeState: SendStates.FeeState? = null, val isBalanceHidden: Boolean, + val isSubtracted: Boolean, val event: StateEvent, -) +) { + + fun getAmountState(isEditState: Boolean): SendStates.AmountState? { + return if (isEditState) { + editAmountState + } else { + amountState + } + } + + fun getRecipientState(isEditState: Boolean): SendStates.RecipientState? { + return if (isEditState) { + editRecipientState + } else { + recipientState + } + } + + fun getFeeState(isEditState: Boolean): SendStates.FeeState? { + return if (isEditState) { + editFeeState + } else { + feeState + } + } + + fun copyWrapped( + isEditState: Boolean, + amountState: SendStates.AmountState? = this.amountState, + feeState: SendStates.FeeState? = this.feeState, + recipientState: SendStates.RecipientState? = this.recipientState, + sendState: SendStates.SendState? = this.sendState, + ): SendUiState = if (isEditState) { + copy( + editAmountState = amountState, + editFeeState = feeState, + editRecipientState = recipientState, + ) + } else { + copy( + amountState = amountState, + feeState = feeState, + recipientState = recipientState, + sendState = sendState, + ) + } +} @Stable internal sealed class SendStates { @@ -48,6 +98,7 @@ internal sealed class SendStates { val walletBalance: TextReference, val tokenIconState: TokenIconState, val segmentedButtonConfig: PersistentList, + val selectedButton: Int, val isSegmentedButtonsEnabled: Boolean, val amountTextField: SendTextField.AmountField, val appCurrencyCode: String, @@ -84,12 +135,14 @@ internal sealed class SendStates { @Stable data class SendState( override val type: SendUiStateType = SendUiStateType.Send, - override val isPrimaryButtonEnabled: Boolean = true, + override val isPrimaryButtonEnabled: Boolean = false, val isSending: Boolean, val isSuccess: Boolean, val transactionDate: Long, val txUrl: String, val ignoreAmountReduce: Boolean, + val reduceAmountBy: BigDecimal?, + val reduceAmountTo: BigDecimal?, val isFromConfirmation: Boolean, val showTapHelp: Boolean, val notifications: ImmutableList, @@ -105,6 +158,9 @@ enum class SendUiStateType { None, Recipient, Amount, - Send, Fee, + Send, + EditAmount, + EditRecipient, + EditFee, } \ No newline at end of file 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 079dbfdb6f..e09467fa9c 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 @@ -18,6 +18,9 @@ internal class StateRouter( val currentState: StateFlow get() = mutableCurrentState + val isEditState: Boolean + get() = currentState.value.isFromConfirmation + fun clear() { mutableCurrentState.update { getInitState() } } @@ -31,25 +34,33 @@ internal class StateRouter( when { isSuccess -> popBackStack() isEditingDisabled -> when (type) { - SendUiStateType.Send -> showFee() + SendUiStateType.EditFee -> showSend() else -> popBackStack() } else -> when (type) { - SendUiStateType.Amount -> continueToSend(::showRecipient) + SendUiStateType.Amount -> showRecipient() SendUiStateType.Fee -> showSend() - SendUiStateType.Send -> continueToSend(::showAmount) - else -> continueToSend(::popBackStack) + SendUiStateType.Send -> showAmount() + SendUiStateType.Recipient -> popBackStack() + SendUiStateType.EditAmount -> showSend() + SendUiStateType.EditRecipient -> showSend() + SendUiStateType.EditFee -> showSend() + else -> popBackStack() } } } fun onNextClick() { when (currentState.value.type) { - SendUiStateType.Recipient -> continueToSend(::showAmount) + SendUiStateType.Recipient -> showAmount() SendUiStateType.Amount, SendUiStateType.Fee, + SendUiStateType.EditAmount, + SendUiStateType.EditRecipient, + SendUiStateType.EditFee, -> showSend() SendUiStateType.Send -> onBackClick() + else -> popBackStack() } } @@ -67,17 +78,35 @@ internal class StateRouter( fun showAmount(isFromConfirmation: Boolean = false) { analyticsEventsHandler.send(SendAnalyticEvents.AmountScreenOpened) - mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Amount, isFromConfirmation) } + mutableCurrentState.update { + if (isFromConfirmation) { + SendUiCurrentScreen(SendUiStateType.EditAmount, true) + } else { + SendUiCurrentScreen(SendUiStateType.Amount, false) + } + } } fun showRecipient(isFromConfirmation: Boolean = false) { analyticsEventsHandler.send(SendAnalyticEvents.AddressScreenOpened) - mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Recipient, isFromConfirmation) } + mutableCurrentState.update { + if (isFromConfirmation) { + SendUiCurrentScreen(SendUiStateType.EditRecipient, true) + } else { + SendUiCurrentScreen(SendUiStateType.Recipient, false) + } + } } fun showFee(isFromConfirmation: Boolean = false) { analyticsEventsHandler.send(SendAnalyticEvents.FeeScreenOpened) - mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Fee, isFromConfirmation) } + mutableCurrentState.update { + if (isFromConfirmation) { + SendUiCurrentScreen(SendUiStateType.EditFee, true) + } else { + SendUiCurrentScreen(SendUiStateType.Fee, false) + } + } } fun showSend() { @@ -85,10 +114,6 @@ internal class StateRouter( mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Send, isFromConfirmation = false) } } - private fun continueToSend(show: () -> Unit) { - if (currentState.value.isFromConfirmation) showSend() else show() - } - private fun getInitState() = if (isEditingDisabled) { SendUiCurrentScreen( type = SendUiStateType.None, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt index 6f56b6aff3..715c696b71 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt @@ -2,26 +2,31 @@ 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.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldMaxAmountConverter import com.tangem.utils.Provider +import java.math.BigDecimal /** * Factory to produce amount state for [SendUiState] */ internal class AmountStateFactory( + private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, ) { private val amountFieldChangeConverter by lazy(LazyThreadSafetyMode.NONE) { SendAmountFieldChangeConverter( + stateRouterProvider = stateRouterProvider, currentStateProvider = currentStateProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } private val amountFieldMaxAmountConverter by lazy(LazyThreadSafetyMode.NONE) { SendAmountFieldMaxAmountConverter( + stateRouterProvider = stateRouterProvider, currentStateProvider = currentStateProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) @@ -29,6 +34,27 @@ internal class AmountStateFactory( private val amountCurrencyConverter by lazy(LazyThreadSafetyMode.NONE) { SendAmountCurrencyConverter( + stateRouterProvider = stateRouterProvider, + currentStateProvider = currentStateProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + private val amountPasteConverter by lazy(LazyThreadSafetyMode.NONE) { + SendAmountPastedTriggerDismissConverter( + stateRouterProvider = stateRouterProvider, + currentStateProvider = currentStateProvider, + ) + } + private val amountReduceByConverter by lazy { + SendAmountReduceByConverter( + stateRouterProvider = stateRouterProvider, + currentStateProvider = currentStateProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + private val amountReduceToConverter by lazy { + SendAmountReduceToConverter( + stateRouterProvider = stateRouterProvider, currentStateProvider = currentStateProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) @@ -36,9 +62,14 @@ internal class AmountStateFactory( fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value) + fun getOnAmountReduceByState(reduceAmountBy: BigDecimal) = amountReduceByConverter.convert(reduceAmountBy) + fun getOnAmountReduceToState(reduceAmountTo: BigDecimal) = amountReduceToConverter.convert(reduceAmountTo) + fun getOnMaxAmountClick(): SendUiState { return amountFieldMaxAmountConverter.convert(Unit) } fun getOnCurrencyChangedState(isFiat: Boolean) = amountCurrencyConverter.convert(isFiat) + + fun getOnAmountPastedTriggerDismiss() = amountPasteConverter.convert(false) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountUtils.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountUtils.kt new file mode 100644 index 0000000000..3921429828 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountUtils.kt @@ -0,0 +1,59 @@ +package com.tangem.features.send.impl.presentation.state.amount + +import androidx.compose.ui.text.input.ImeAction +import com.tangem.common.extensions.isZero +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import java.math.BigDecimal +import java.math.RoundingMode + +internal fun String.getCryptoValue(fiatRate: BigDecimal?, isFiatValue: Boolean, decimals: Int): String { + return if (isFiatValue && fiatRate != null) { + parseToBigDecimal(decimals).divide(fiatRate, decimals, RoundingMode.DOWN) + .parseBigDecimal(decimals) + } else { + this + } +} + +internal fun String.getFiatValue( + fiatRate: BigDecimal?, + isFiatValue: Boolean, + decimals: Int, +): Pair { + return if (fiatRate != null) { + val fiatValue = if (!isFiatValue) { + parseToBigDecimal(decimals).multiply(fiatRate).parseBigDecimal(decimals) + } else { + this + } + val decimalFiatValue = fiatValue.parseToBigDecimal(decimals) + fiatValue to decimalFiatValue + } else { + "" to null + } +} + +internal fun String.checkExceedBalance( + cryptoCurrencyStatus: CryptoCurrencyStatus, + amountTextField: SendTextField.AmountField, +): Boolean { + val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO + val fiatDecimal = parseToBigDecimal(amountTextField.fiatAmount.decimals) + val cryptoDecimal = parseToBigDecimal(amountTextField.cryptoAmount.decimals) + return if (amountTextField.isFiatValue) { + fiatDecimal > currencyFiatAmount + } else { + cryptoDecimal > currencyCryptoAmount + } +} + +internal fun getKeyboardAction(isExceedBalance: Boolean, decimalCryptoValue: BigDecimal) = + if (!isExceedBalance && !decimalCryptoValue.isZero()) { + ImeAction.Done + } else { + ImeAction.None + } \ 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 index 0f95e9fad7..fa31b63ea0 100644 --- 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 @@ -1,30 +1,44 @@ package com.tangem.features.send.impl.presentation.state.amount +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero internal class SendAmountCurrencyConverter( + private val stateRouterProvider: Provider, 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 isEditState = stateRouterProvider().isEditState + val amountState = state.getAmountState(isEditState) ?: return state val amountTextField = amountState.amountTextField val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isValidFiatRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero() + val isDoneActionEnabled = amountState.isPrimaryButtonEnabled return if (amountTextField.isFiatValue == value && !isValidFiatRate) { state } else { - return state.copy( + return state.copyWrapped( + isEditState = isEditState, amountState = amountState.copy( amountTextField = amountTextField.copy( isFiatValue = value, + isValuePasted = true, + keyboardOptions = KeyboardOptions( + imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, + keyboardType = KeyboardType.Number, + ), ), + selectedButton = amountState.segmentedButtonConfig.indexOfFirst { it.isFiat == value }, ), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt new file mode 100644 index 0000000000..093b1b6a8e --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt @@ -0,0 +1,25 @@ +package com.tangem.features.send.impl.presentation.state.amount + +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.StateRouter +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter + +internal class SendAmountPastedTriggerDismissConverter( + private val stateRouterProvider: Provider, + private val currentStateProvider: Provider, +) : Converter { + override fun convert(value: Boolean): SendUiState { + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val amountState = state.getAmountState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, + amountState = amountState.copy( + amountTextField = amountState.amountTextField.copy( + isValuePasted = false, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt new file mode 100644 index 0000000000..59dadf2844 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt @@ -0,0 +1,62 @@ +package com.tangem.features.send.impl.presentation.state.amount + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.extensions.isZero +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.StateRouter +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero +import java.math.BigDecimal + +internal class SendAmountReduceByConverter( + private val stateRouterProvider: Provider, + private val currentStateProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + override fun convert(value: BigDecimal): SendUiState { + val state = currentStateProvider() + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val isEditState = stateRouterProvider().isEditState + val amountState = state.getAmountState(isEditState) ?: return state + val amountTextField = amountState.amountTextField + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + val amountValue = amountState.amountTextField.cryptoAmount.value ?: return state + + val decimalCryptoValue = amountValue.minus(value) + val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals) + val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue( + fiatRate = cryptoCurrencyStatus.value.fiatRate, + isFiatValue = false, + decimals = fiatDecimals, + ) + + val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue + val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) + val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else decimalCryptoValue.isZero() + return state.copyWrapped( + isEditState = isEditState, + sendState = state.sendState?.copy( + reduceAmountBy = value, + ), + amountState = amountState.copy( + isPrimaryButtonEnabled = !isExceedBalance && !isZero, + amountTextField = amountTextField.copy( + value = cryptoValue, + fiatValue = fiatValue, + isError = isExceedBalance, + cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + keyboardOptions = KeyboardOptions( + imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue), + keyboardType = KeyboardType.Number, + ), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt new file mode 100644 index 0000000000..5b40022bbb --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt @@ -0,0 +1,60 @@ +package com.tangem.features.send.impl.presentation.state.amount + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.extensions.isZero +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.StateRouter +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero +import java.math.BigDecimal + +internal class SendAmountReduceToConverter( + private val stateRouterProvider: Provider, + private val currentStateProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + override fun convert(value: BigDecimal): SendUiState { + val state = currentStateProvider() + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val isEditState = stateRouterProvider().isEditState + val amountState = state.getAmountState(isEditState) ?: return state + val amountTextField = amountState.amountTextField + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + + val cryptoValue = value.parseBigDecimal(cryptoDecimals) + val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue( + fiatRate = cryptoCurrencyStatus.value.fiatRate, + isFiatValue = false, + decimals = fiatDecimals, + ) + + val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue + val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) + val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else value.isZero() + return state.copyWrapped( + isEditState = isEditState, + sendState = state.sendState?.copy( + reduceAmountBy = value, + ), + amountState = amountState.copy( + isPrimaryButtonEnabled = !isExceedBalance && !isZero, + amountTextField = amountTextField.copy( + value = cryptoValue, + fiatValue = fiatValue, + isError = isExceedBalance, + cryptoAmount = amountTextField.cryptoAmount.copy(value = value), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + keyboardOptions = KeyboardOptions( + imeAction = getKeyboardAction(isExceedBalance, value), + keyboardType = KeyboardType.Number, + ), + ), + ), + ) + } +} \ No newline at end of file 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 7e3199d1ff..6afdc1ada8 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 @@ -57,6 +57,7 @@ internal class SendAmountStateConverter( ), ), isSegmentedButtonsEnabled = !noFeeRate, + selectedButton = 0, ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/common/SendSyncEditConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/common/SendSyncEditConverter.kt new file mode 100644 index 0000000000..f0abbba036 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/common/SendSyncEditConverter.kt @@ -0,0 +1,26 @@ +package com.tangem.features.send.impl.presentation.state.common + +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter + +internal class SendSyncEditConverter( + private val currentStateProvider: Provider, +) : Converter { + override fun convert(value: Boolean): SendUiState { + val state = currentStateProvider() + return if (value) { + state.copy( + amountState = state.editAmountState, + feeState = state.editFeeState, + recipientState = state.editRecipientState, + ) + } else { + state.copy( + editAmountState = state.amountState, + editRecipientState = state.recipientState, + editFeeState = state.feeState, + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendConfirmStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendConfirmStateConverter.kt index e202bf5e25..6e6e67132b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendConfirmStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendConfirmStateConverter.kt @@ -10,12 +10,14 @@ internal class SendConfirmStateConverter( ) : Converter { override fun convert(value: Unit): SendStates.SendState { return SendStates.SendState( - isPrimaryButtonEnabled = true, + isPrimaryButtonEnabled = false, isSending = false, isSuccess = false, transactionDate = 0L, txUrl = "", ignoreAmountReduce = false, + reduceAmountBy = null, + reduceAmountTo = null, isFromConfirmation = true, showTapHelp = isTapHelpPreviewEnabledProvider(), notifications = persistentListOf(), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 563ec2af7f..baa881f4d6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -9,6 +9,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.networkIconResId 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.GetBalanceNotEnoughForFeeWarningUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -19,8 +20,9 @@ import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents 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.fields.SendTextField +import com.tangem.features.send.impl.presentation.utils.getFiatString import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents -import com.tangem.lib.crypto.BlockchainUtils.isDogecoin import com.tangem.lib.crypto.BlockchainUtils.isTezos import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList @@ -31,7 +33,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import java.math.BigDecimal -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class SendNotificationFactory( private val cryptoCurrencyStatusProvider: Provider, private val coinCryptoCurrencyStatusProvider: Provider, @@ -40,6 +42,7 @@ internal class SendNotificationFactory( private val currencyChecksRepository: CurrencyChecksRepository, private val stateRouterProvider: Provider, private val isSubtractAvailableProvider: Provider, + private val appCurrencyProvider: Provider, private val clickIntents: SendClickIntents, private val analyticsEventHandler: AnalyticsEventHandler, private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, @@ -49,10 +52,13 @@ internal class SendNotificationFactory( .filter { it.type == SendUiStateType.Send } .map { val state = currentStateProvider() - val sendState = state.sendState ?: return@map persistentListOf() - val feeState = state.feeState ?: return@map persistentListOf() + val isEditState = stateRouterProvider().isEditState val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO - val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO + val sendState = state.sendState ?: return@map persistentListOf() + val feeState = state.getFeeState(isEditState) ?: return@map persistentListOf() + val amountState = state.getAmountState(isEditState) ?: return@map persistentListOf() + + val amountValue = amountState.amountTextField.cryptoAmount.value ?: BigDecimal.ZERO val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO val isFeeCoverage = checkFeeCoverage( isSubtractAvailable = isSubtractAvailableProvider(), @@ -71,12 +77,15 @@ internal class SendNotificationFactory( addFeeUnreachableNotification(feeState.feeSelectorState) addExceedBalanceNotification(feeValue, sendingAmount) addExceedsBalanceNotification(feeState.fee) - addMinimumAmountErrorNotification(feeValue, sendingAmount) addDustWarningNotification(feeValue, sendingAmount) addTransactionLimitErrorNotification(feeValue, sendingAmount) // warnings addExistentialWarningNotification(feeValue, amountValue) - addFeeCoverageNotification(isFeeCoverage) + addFeeCoverageNotification( + isFeeCoverage = isFeeCoverage, + amountField = amountState.amountTextField, + sendingValue = sendingAmount, + ) addHighFeeWarningNotification(amountValue, sendState.ignoreAmountReduce) addTooHighNotification(feeState.feeSelectorState) addTooLowNotification(feeState) @@ -92,6 +101,7 @@ internal class SendNotificationFactory( return state.copy( sendState = sendState.copy( ignoreAmountReduce = isIgnored, + reduceAmountBy = if (isIgnored) null else sendState.reduceAmountBy, notifications = updatedNotifications.toImmutableList(), ), ) @@ -164,8 +174,8 @@ internal class SendNotificationFactory( ), onConfirmClick = { clickIntents.onAmountReduceClick( - utxoLimit.maxAmount, - SendNotification.Error.TransactionLimitError::class.java, + reduceAmountTo = utxoLimit.maxAmount, + clazz = SendNotification.Error.TransactionLimitError::class.java, ) }, ), @@ -191,22 +201,50 @@ internal class SendNotificationFactory( cryptoCurrency.network, ) val diff = balance.minus(spendingAmount) - if (currencyDeposit != null && currencyDeposit > diff) { + if (currencyDeposit != null && diff >= BigDecimal.ZERO && currencyDeposit > diff) { add( SendNotification.Error.ExistentialDeposit( - BigDecimalFormatter.formatCryptoAmountUncapped( + deposit = BigDecimalFormatter.formatCryptoAmountUncapped( cryptoAmount = currencyDeposit, cryptoCurrency = cryptoCurrency, ), + onConfirmClick = { + clickIntents.onAmountReduceClick( + reduceAmountBy = currencyDeposit, + clazz = SendNotification.Error.ExistentialDeposit::class.java, + ) + }, ), ) } } - private fun MutableList.addFeeCoverageNotification(sendingAmount: Boolean) { - if (sendingAmount) { + private fun MutableList.addFeeCoverageNotification( + isFeeCoverage: Boolean, + amountField: SendTextField.AmountField, + sendingValue: BigDecimal, + ) { + if (isFeeCoverage) { analyticsEventHandler.send(SendAnalyticEvents.NoticeFeeCoverage) - add(SendNotification.Warning.FeeCoverageNotification) + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + val fiatRate = cryptoCurrencyStatus.value.fiatRate + val amountValue = amountField.cryptoAmount.value ?: return + + val cryptoDiff = amountValue.minus(sendingValue) + add( + SendNotification.Warning.FeeCoverageNotification( + cryptoAmount = BigDecimalFormatter.formatCryptoAmountUncapped( + cryptoAmount = cryptoDiff, + cryptoCurrency = cryptoCurrency, + ), + fiatAmount = getFiatString( + value = cryptoDiff, + rate = fiatRate, + appCurrency = appCurrencyProvider(), + ), + ), + ) } } @@ -224,8 +262,10 @@ internal class SendNotificationFactory( SendNotification.Warning.HighFeeError( amount = threshold.toPlainString(), onConfirmClick = { - val reduceTo = sendAmount.minus(threshold) - clickIntents.onAmountReduceClick(reduceTo, SendNotification.Warning.HighFeeError::class.java) + clickIntents.onAmountReduceClick( + reduceAmountBy = threshold, + clazz = SendNotification.Warning.HighFeeError::class.java, + ) }, onCloseClick = { clickIntents.onNotificationCancel(SendNotification.Warning.HighFeeError::class.java) @@ -235,21 +275,6 @@ internal class SendNotificationFactory( } } - // todo remove in [REDACTED_TASK_KEY] - private fun MutableList.addMinimumAmountErrorNotification( - feeAmount: BigDecimal, - receivedAmount: BigDecimal, - ) { - val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider() - val minimum = BigDecimal(DOGECOIN_MINIMUM) - - val isDogecoin = isDogecoin(coinCryptoCurrencyStatus.currency.network.id.value) - val isExceedDustLimit = checkDustLimits(feeAmount, receivedAmount, minimum) - if (isDogecoin && isExceedDustLimit) { - add(SendNotification.Error.MinimumAmountError(DOGECOIN_MINIMUM)) - } - } - private suspend fun MutableList.addDustWarningNotification( feeAmount: BigDecimal, receivedAmount: BigDecimal, @@ -377,8 +402,4 @@ internal class SendNotificationFactory( val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO return receivedAmount < dustValue || isChangeLowerThanDust } - - companion object { - private const val DOGECOIN_MINIMUM = "0.01" - } } \ No newline at end of file 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 5a9062da56..b06d42a23d 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,13 +1,10 @@ package com.tangem.features.send.impl.presentation.state.fee -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.blockchainsdk.utils.minimalAmount +import com.tangem.common.extensions.isZero import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.send.impl.presentation.state.SendUiState -import com.tangem.lib.crypto.BlockchainUtils import java.math.BigDecimal import java.math.RoundingMode @@ -19,6 +16,7 @@ internal fun checkAndCalculateSubtractedAmount( cryptoCurrencyStatus: CryptoCurrencyStatus, amountValue: BigDecimal, feeValue: BigDecimal, + reduceAmountBy: BigDecimal?, ): BigDecimal { val balance = cryptoCurrencyStatus.value.amount ?: return amountValue val isFeeCoverage = checkFeeCoverage( @@ -27,12 +25,17 @@ internal fun checkAndCalculateSubtractedAmount( amountValue = amountValue, feeValue = feeValue, ) - return calculateSubtractedAmount( + val subtractedAmount = calculateSubtractedAmount( isFeeCoverage = isFeeCoverage, cryptoCurrencyStatus = cryptoCurrencyStatus, amountValue = amountValue, feeValue = feeValue, ) + return if (reduceAmountBy != null) { + subtractedAmount.minus(reduceAmountBy) + } else { + subtractedAmount + } } /** @@ -45,7 +48,7 @@ internal fun checkFeeCoverage( feeValue: BigDecimal, ): Boolean { if (!isSubtractAvailable) return false - return balance < amountValue + feeValue && balance > feeValue + return balance < amountValue + feeValue && balance > feeValue && balance >= amountValue } /** @@ -59,12 +62,7 @@ internal fun calculateSubtractedAmount( ): BigDecimal { val balance = cryptoCurrencyStatus.value.amount ?: return amountValue return if (isFeeCoverage) { - var subtractedValue = minOf(amountValue, balance.minus(feeValue)) - if (BlockchainUtils.isTezos(cryptoCurrencyStatus.currency.network.id.value)) { - val threshold = Blockchain.Tezos.minimalAmount() - subtractedValue = -threshold - } - subtractedValue + minOf(amountValue, balance.minus(feeValue)) } else { amountValue } @@ -73,8 +71,7 @@ internal fun calculateSubtractedAmount( /** * Check if custom fee is too low */ -internal fun checkIfFeeTooLow(state: SendUiState): Boolean { - val feeSelectorState = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false +internal fun checkIfFeeTooLow(feeSelectorState: FeeSelectorState.Content): Boolean { val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return false val minimumValue = multipleFees.minimum.amount.value ?: return false val customAmount = feeSelectorState.customValues.firstOrNull() ?: return false @@ -97,5 +94,12 @@ internal fun checkIfFeeTooHigh(feeSelectorState: FeeSelectorState.Content, onSho return isShow } +/** + * Checks if fee exceeds fee paid currency balance + */ +fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean { + return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance +} + private val FEE_MAX_DIFF = BigDecimal("5") private const val ZERO_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/FeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt index 1fa99ee291..5109aa17b9 100644 --- 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 @@ -5,6 +5,7 @@ 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.StateRouter import com.tangem.features.send.impl.presentation.state.fee.custom.BitcoinCustomFeeConverter import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents @@ -13,6 +14,7 @@ import com.tangem.utils.converter.Converter internal class FeeConverter( private val clickIntents: SendClickIntents, + private val stateRouterProvider: Provider, private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter { @@ -20,6 +22,7 @@ internal class FeeConverter( private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { EthereumCustomFeeConverter( clickIntents = clickIntents, + stateRouterProvider = stateRouterProvider, appCurrencyProvider = appCurrencyProvider, feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) @@ -28,6 +31,7 @@ internal class FeeConverter( private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { BitcoinCustomFeeConverter( clickIntents = clickIntents, + stateRouterProvider = stateRouterProvider, appCurrencyProvider = appCurrencyProvider, feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) 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 92189a24ef..229568a930 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,9 +1,6 @@ package com.tangem.features.send.impl.presentation.state.fee -import com.tangem.features.send.impl.presentation.state.SendNotification -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.StateRouter +import com.tangem.features.send.impl.presentation.state.* import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import kotlinx.collections.immutable.persistentListOf @@ -19,10 +16,10 @@ internal class FeeNotificationFactory( ) { fun create() = stateRouterProvider().currentState - .filter { it.type == SendUiStateType.Fee } + .filter { it.type == SendUiStateType.Fee || it.type == SendUiStateType.EditFee } .map { val state = currentStateProvider() - val feeState = state.feeState ?: return@map persistentListOf() + val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return@map persistentListOf() buildList { addFeeUnreachableNotification(feeState.feeSelectorState) }.toImmutableList() 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 a74613acd3..e933b89477 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 @@ -10,6 +10,7 @@ import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.features.send.impl.presentation.state.SendNotification 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.StateRouter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList @@ -20,6 +21,7 @@ import kotlinx.collections.immutable.persistentListOf */ internal class FeeStateFactory( private val clickIntents: SendClickIntents, + private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val feeCryptoCurrencyStatusProvider: Provider, private val appCurrencyProvider: Provider, @@ -28,6 +30,7 @@ internal class FeeStateFactory( private val customFeeFieldConverter by lazy(LazyThreadSafetyMode.NONE) { SendFeeCustomFieldConverter( clickIntents = clickIntents, + stateRouterProvider = stateRouterProvider, appCurrencyProvider = appCurrencyProvider, feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) @@ -36,6 +39,7 @@ internal class FeeStateFactory( val feeConverter by lazy(LazyThreadSafetyMode.NONE) { FeeConverter( clickIntents = clickIntents, + stateRouterProvider = stateRouterProvider, appCurrencyProvider = appCurrencyProvider, feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) @@ -43,8 +47,13 @@ internal class FeeStateFactory( fun onFeeOnLoadingState(): SendUiState { val state = currentStateProvider() - val feeState = state.feeState ?: return state - return state.copy( + val isEditState = stateRouterProvider().isEditState + val feeState = state.getFeeState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, + sendState = state.sendState?.copy( + isPrimaryButtonEnabled = false, + ), feeState = feeState.copy( feeSelectorState = if (feeState.feeSelectorState is FeeSelectorState.Content) { feeState.feeSelectorState @@ -59,7 +68,8 @@ internal class FeeStateFactory( fun onFeeOnLoadedState(fees: TransactionFee): SendUiState { val state = currentStateProvider() - val feeState = state.feeState ?: return state + val isEditState = stateRouterProvider().isEditState + val feeState = state.getFeeState(isEditState) ?: return state val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content val isCustomWasSelected = if (feeState.isCustomSelected) { @@ -76,7 +86,11 @@ internal class FeeStateFactory( ) val fee = feeConverter.convert(updatedFeeSelectorState) - return state.copy( + return state.copyWrapped( + isEditState = isEditState, + sendState = state.sendState?.copy( + isPrimaryButtonEnabled = true, + ), feeState = feeState.copy( feeSelectorState = updatedFeeSelectorState, fee = fee, @@ -87,22 +101,29 @@ internal class FeeStateFactory( fun onFeeOnErrorState(): SendUiState { val state = currentStateProvider() - return state.copy( - feeState = state.feeState?.copy( + val isEditState = stateRouterProvider().isEditState + return state.copyWrapped( + isEditState = isEditState, + feeState = state.getFeeState(isEditState)?.copy( feeSelectorState = FeeSelectorState.Error, ), + sendState = state.sendState?.copy( + isPrimaryButtonEnabled = false, + ), ) } fun onFeeSelectedState(feeType: FeeType): SendUiState { val state = currentStateProvider() - val feeState = state.feeState ?: return state + val isEditState = stateRouterProvider().isEditState + val feeState = state.getFeeState(isEditState) ?: return state val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType) val fee = feeConverter.convert(updatedFeeSelectorState) val isCustomFeeWasSelected = feeState.isCustomSelected || updatedFeeSelectorState.selectedFee == FeeType.Custom - return state.copy( + return state.copyWrapped( + isEditState = isEditState, feeState = feeState.copy( fee = fee, isCustomSelected = isCustomFeeWasSelected, @@ -113,12 +134,14 @@ internal class FeeStateFactory( fun onCustomFeeValueChange(index: Int, value: String): SendUiState { val state = currentStateProvider() - val feeState = state.feeState ?: return state + val isEditState = stateRouterProvider().isEditState + val feeState = state.getFeeState(isEditState) ?: return state val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state val updatedFeeSelectorState = customFeeFieldConverter.onValueChange(feeSelectorState, index, value) val fee = feeConverter.convert(updatedFeeSelectorState) - return state.copy( + return state.copyWrapped( + isEditState = isEditState, feeState = feeState.copy( feeSelectorState = updatedFeeSelectorState, fee = fee, @@ -128,10 +151,13 @@ internal class FeeStateFactory( fun getFeeNotificationState(notifications: ImmutableList): SendUiState { val state = currentStateProvider() - return state.copy( - feeState = state.feeState?.copy( + val isEditState = stateRouterProvider().isEditState + val feeState = state.getFeeState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, + feeState = feeState.copy( notifications = notifications, - isPrimaryButtonEnabled = isPrimaryButtonEnabled(state.feeState, notifications), + isPrimaryButtonEnabled = isPrimaryButtonEnabled(feeState, notifications), ), ) } 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 c9534ea97f..5fd2a4060d 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 @@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.state.fee import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fee.custom.BitcoinCustomFeeConverter import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter import com.tangem.features.send.impl.presentation.state.fields.SendTextField @@ -14,6 +15,7 @@ import kotlinx.collections.immutable.persistentListOf internal class SendFeeCustomFieldConverter( private val clickIntents: SendClickIntents, + private val stateRouterProvider: Provider, private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter> { @@ -21,6 +23,7 @@ internal class SendFeeCustomFieldConverter( private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { EthereumCustomFeeConverter( clickIntents = clickIntents, + stateRouterProvider = stateRouterProvider, appCurrencyProvider = appCurrencyProvider, feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) @@ -29,6 +32,7 @@ internal class SendFeeCustomFieldConverter( private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { BitcoinCustomFeeConverter( clickIntents = clickIntents, + stateRouterProvider = stateRouterProvider, appCurrencyProvider = appCurrencyProvider, feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt index 8226a67504..a2a13e987a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt @@ -5,17 +5,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.common.extensions.isZero -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.StateRouter +import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.utils.getFiatReference import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.Provider @@ -27,12 +26,14 @@ import java.math.RoundingMode internal class BitcoinCustomFeeConverter( private val clickIntents: SendClickIntents, + private val stateRouterProvider: Provider, private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatusProvider: Provider, ) : CustomFeeConverter { override fun convert(value: Fee.Bitcoin): ImmutableList { val feeValue = value.amount.value + val feeCurrency = feeCryptoCurrencyStatusProvider()?.value val network = feeCryptoCurrencyStatusProvider()?.currency?.network?.id?.value return if (network != null && isBitcoin(network)) { persistentListOf( @@ -47,7 +48,11 @@ internal class BitcoinCustomFeeConverter( ), title = resourceReference(R.string.send_max_fee), footer = resourceReference(R.string.send_max_fee_footer), - label = getFeeFormatted(feeValue), + label = getFiatReference( + rate = feeCurrency?.fiatRate, + value = feeValue, + appCurrency = appCurrencyProvider(), + ), keyboardActions = KeyboardActions(), isReadonly = true, ), @@ -63,10 +68,20 @@ internal class BitcoinCustomFeeConverter( footer = resourceReference(R.string.send_satoshi_per_byte_text), onValueChange = { clickIntents.onCustomFeeValueChange(FEE_SATOSHI_INDEX, it) }, keyboardOptions = KeyboardOptions( - imeAction = if (checkExceedBalance(feeValue)) ImeAction.None else ImeAction.Done, + imeAction = if (checkExceedBalance( + feeBalance = feeCurrency?.amount, + feeAmount = feeValue, + ) + ) { + ImeAction.None + } else { + ImeAction.Done + }, keyboardType = KeyboardType.Number, ), - keyboardActions = KeyboardActions(), + keyboardActions = KeyboardActions( + onDone = { clickIntents.onNextClick(stateRouterProvider().isEditState) }, + ), ), ) } else { @@ -100,7 +115,11 @@ internal class BitcoinCustomFeeConverter( FEE_AMOUNT_INDEX, this[FEE_AMOUNT_INDEX].copy( value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT_INDEX].decimals), - label = getFeeFormatted(newFeeAmount), + label = getFiatReference( + rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate, + value = newFeeAmount, + appCurrency = appCurrencyProvider(), + ), ), ) set(index, this[index].copy(value = value)) @@ -108,26 +127,6 @@ internal class BitcoinCustomFeeConverter( }.toImmutableList() } - private fun getFeeFormatted(fee: BigDecimal?): TextReference { - val appCurrency = appCurrencyProvider() - val rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate - val fiatFee = rate?.let { fee?.multiply(it) } - return stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatFee, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), - ) - } - - private fun checkExceedBalance(feeAmount: BigDecimal?): Boolean { - val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider() - val currencyCryptoAmount = cryptoCurrencyStatus?.value?.amount ?: BigDecimal.ZERO - - return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount - } - private fun toSatoshiPerByte(amount: BigDecimal?, decimals: Int, txSize: BigDecimal): BigDecimal? { val newFeeAmount = amount?.movePointRight(decimals) return newFeeAmount?.divide( 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 index 4851533b49..6bed843250 100644 --- 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 @@ -5,33 +5,33 @@ 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.common.extensions.isZero -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.StateRouter +import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.utils.getFiatReference 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 import java.math.RoundingMode internal class EthereumCustomFeeConverter( private val clickIntents: SendClickIntents, + private val stateRouterProvider: Provider, private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatusProvider: Provider, ) : CustomFeeConverter { override fun convert(value: Fee.Ethereum): ImmutableList { val feeValue = value.amount.value + val feeCurrency = feeCryptoCurrencyStatusProvider()?.value return persistentListOf( SendTextField.CustomFee( value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(), @@ -44,7 +44,11 @@ internal class EthereumCustomFeeConverter( ), title = resourceReference(R.string.send_max_fee), footer = resourceReference(R.string.send_max_fee_footer), - label = getFeeFormatted(feeValue), + label = getFiatReference( + rate = feeCurrency?.fiatRate, + value = feeValue, + appCurrency = appCurrencyProvider(), + ), keyboardActions = KeyboardActions(), ), SendTextField.CustomFee( @@ -68,10 +72,21 @@ internal class EthereumCustomFeeConverter( footer = resourceReference(R.string.send_gas_limit_footer), onValueChange = { clickIntents.onCustomFeeValueChange(GAS_LIMIT, it) }, keyboardOptions = KeyboardOptions( - imeAction = if (checkExceedBalance(feeValue)) ImeAction.None else ImeAction.Done, + imeAction = if ( + checkExceedBalance( + feeBalance = feeCurrency?.amount, + feeAmount = feeValue, + ) + ) { + ImeAction.None + } else { + ImeAction.Done + }, keyboardType = KeyboardType.Number, ), - keyboardActions = KeyboardActions(onDone = { clickIntents.onNextClick() }), + keyboardActions = KeyboardActions( + onDone = { clickIntents.onNextClick(stateRouterProvider().isEditState) }, + ), ), ) } @@ -102,26 +117,6 @@ internal class EthereumCustomFeeConverter( }.toImmutableList() } - private fun getFeeFormatted(fee: BigDecimal?): TextReference { - val appCurrency = appCurrencyProvider() - val rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate - val fiatFee = rate?.let { fee?.multiply(it) } - return stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatFee, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), - ) - } - - private fun checkExceedBalance(feeAmount: BigDecimal?): Boolean { - val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider() - val currencyCryptoAmount = cryptoCurrencyStatus?.value?.amount ?: BigDecimal.ZERO - - return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount - } - private fun MutableList.setEmpty(index: Int) { set(index, this[index].copy(value = "")) } @@ -140,7 +135,11 @@ internal class EthereumCustomFeeConverter( index, this[index].copy( value = value, - label = getFeeFormatted(newFeeAmountDecimal), + label = getFiatReference( + rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate, + value = newFeeAmountDecimal, + appCurrency = appCurrencyProvider(), + ), ), ) } @@ -159,7 +158,11 @@ internal class EthereumCustomFeeConverter( FEE_AMOUNT, this[FEE_AMOUNT].copy( value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals), - label = getFeeFormatted(newFeeAmount), + label = getFiatReference( + rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate, + value = newFeeAmount, + appCurrency = appCurrencyProvider(), + ), ), ) set(index, this[index].copy(value = value)) @@ -179,15 +182,23 @@ internal class EthereumCustomFeeConverter( FEE_AMOUNT, this[FEE_AMOUNT].copy( value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals), - label = getFeeFormatted(newFeeAmount), + label = getFiatReference( + rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate, + value = newFeeAmount, + appCurrency = appCurrencyProvider(), + ), ), ) + val isNotExceedBalance = checkExceedBalance( + feeBalance = feeCryptoCurrencyStatusProvider()?.value?.amount, + feeAmount = newFeeAmount, + ) set( index, this[index].copy( value = value, keyboardOptions = KeyboardOptions( - imeAction = if (!checkExceedBalance(newFeeAmount)) ImeAction.None else ImeAction.Done, + imeAction = if (!isNotExceedBalance) ImeAction.None else ImeAction.Done, keyboardType = KeyboardType.Number, ), ), 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 a1fc8fbe25..3958e3b64a 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,25 +1,31 @@ package com.tangem.features.send.impl.presentation.state.fields import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.extensions.isZero -import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus 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.amount.checkExceedBalance +import com.tangem.features.send.impl.presentation.state.amount.getCryptoValue +import com.tangem.features.send.impl.presentation.state.amount.getFiatValue +import com.tangem.features.send.impl.presentation.state.amount.getKeyboardAction import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero import java.math.BigDecimal -import java.math.RoundingMode internal class SendAmountFieldChangeConverter( + private val stateRouterProvider: Provider, 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 cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val isEditState = stateRouterProvider().isEditState + val amountState = state.getAmountState(isEditState) ?: return state val amountTextField = amountState.amountTextField if (value.isEmpty()) return state.emptyState() @@ -27,15 +33,23 @@ internal class SendAmountFieldChangeConverter( val fiatDecimals = amountTextField.fiatAmount.decimals val trimmedValue = value.trim() - val cryptoValue = trimmedValue.getCryptoValue(amountTextField.isFiatValue, cryptoDecimals) - val fiatValue = trimmedValue.getFiatValue(amountTextField.isFiatValue, fiatDecimals) + val cryptoValue = trimmedValue.getCryptoValue( + fiatRate = cryptoCurrencyStatus.value.fiatRate, + isFiatValue = amountTextField.isFiatValue, + decimals = cryptoDecimals, + ) val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals) - val decimalFiatValue = fiatValue.parseToBigDecimal(fiatDecimals) + val (fiatValue, decimalFiatValue) = trimmedValue.getFiatValue( + fiatRate = cryptoCurrencyStatus.value.fiatRate, + isFiatValue = amountTextField.isFiatValue, + decimals = fiatDecimals, + ) val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue - val isExceedBalance = checkValue.checkExceedBalance(amountTextField) - val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isZero() else decimalCryptoValue.isZero() - return state.copy( + val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) + val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else decimalCryptoValue.isZero() + return state.copyWrapped( + isEditState = isEditState, amountState = amountState.copy( isPrimaryButtonEnabled = !isExceedBalance && !isZero, amountTextField = amountTextField.copy( @@ -53,31 +67,12 @@ 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 { - if (amountState == null) return this + val isEditState = stateRouterProvider().isEditState + val amountState = getAmountState(isEditState) ?: return this val amountTextField = amountState.amountTextField - return copy( + return copyWrapped( + isEditState = isEditState, amountState = amountState.copy( isPrimaryButtonEnabled = false, amountTextField = amountTextField.copy( @@ -90,24 +85,4 @@ internal class SendAmountFieldChangeConverter( ), ) } - - 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 - val fiatDecimal = parseToBigDecimal(amountTextField.fiatAmount.decimals) - val cryptoDecimal = parseToBigDecimal(amountTextField.cryptoAmount.decimals) - return if (amountTextField.isFiatValue) { - fiatDecimal > currencyFiatAmount - } else { - cryptoDecimal > currencyCryptoAmount - } - } - - private fun getKeyboardAction(isExceedBalance: Boolean, decimalCryptoValue: BigDecimal) = - if (!isExceedBalance && !decimalCryptoValue.isZero()) { - ImeAction.Done - } else { - ImeAction.None - } } \ 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 332a736cd0..8bf30c0b57 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 @@ -14,15 +14,18 @@ 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.state.StateRouter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero import java.math.BigDecimal private const val FIAT_DECIMALS = 2 internal class SendAmountFieldConverter( private val clickIntents: SendClickIntents, + private val stateRouterProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, private val appCurrencyProvider: Provider, ) : Converter { @@ -32,12 +35,14 @@ internal class SendAmountFieldConverter( val cryptoDecimal = value.toBigDecimalOrDefault() val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency) val fiatRate = cryptoCurrencyStatus.value.fiatRate - val (fiatValue, fiatDecimal) = if (value.isEmpty()) { - "" to BigDecimal.ZERO - } else { - val fiatDecimal = fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO - val fiatValue = fiatDecimal.parseBigDecimal(FIAT_DECIMALS) - fiatValue to fiatDecimal + val (fiatValue, fiatDecimal) = when { + fiatRate.isNullOrZero() -> "" to null + value.isEmpty() -> "" to BigDecimal.ZERO + else -> { + val fiatDecimal = fiatRate?.multiply(cryptoDecimal) + val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty() + fiatValue to fiatDecimal + } } val isDoneActionEnabled = !cryptoDecimal.isZero() return SendTextField.AmountField( @@ -48,17 +53,21 @@ internal class SendAmountFieldConverter( imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, keyboardType = KeyboardType.Number, ), - keyboardActions = KeyboardActions(onDone = { clickIntents.onNextClick() }), + keyboardActions = KeyboardActions( + onDone = { clickIntents.onNextClick(stateRouterProvider().isEditState) }, + ), isFiatValue = false, cryptoAmount = cryptoAmount, fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrencyProvider()), isError = false, - error = TextReference.Res(R.string.swapping_insufficient_funds), + error = TextReference.Res(R.string.send_validation_amount_exceeds_balance), isFiatUnavailable = fiatRate == null, + isValuePasted = false, + onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, ) } - private fun getAppCurrencyAmount(fiatValue: BigDecimal, appCurrency: AppCurrency) = Amount( + private fun getAppCurrencyAmount(fiatValue: BigDecimal?, appCurrency: AppCurrency) = Amount( currencySymbol = appCurrency.symbol, value = fiatValue, decimals = FIAT_DECIMALS, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt index b29459e757..7d4dfe3b45 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt @@ -6,11 +6,13 @@ import androidx.compose.ui.text.input.KeyboardType import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero internal class SendAmountFieldMaxAmountConverter( + private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, ) : Converter { @@ -18,7 +20,8 @@ internal class SendAmountFieldMaxAmountConverter( override fun convert(value: Unit): SendUiState { val state = currentStateProvider() val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val amountState = state.amountState ?: return state + val isEditState = stateRouterProvider().isEditState + val amountState = state.getAmountState(isEditState) ?: return state val amountTextField = amountState.amountTextField val cryptoDecimals = amountTextField.cryptoAmount.decimals @@ -31,10 +34,12 @@ internal class SendAmountFieldMaxAmountConverter( val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero() val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty() val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals).orEmpty() - return state.copy( + return state.copyWrapped( + isEditState = isEditState, amountState = amountState.copy( isPrimaryButtonEnabled = true, amountTextField = amountTextField.copy( + isValuePasted = true, value = cryptoValue, fiatValue = fiatValue, isError = false, 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 266f9e8f6e..6cd4f8c31d 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 @@ -28,6 +28,8 @@ internal sealed class SendTextField { val isFiatValue: Boolean, val fiatValue: String, val isFiatUnavailable: Boolean, + val isValuePasted: Boolean, + val onValuePastedTriggerDismiss: () -> Unit, val isError: Boolean, val error: TextReference, ) : SendTextField() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt index cbe162c0c5..e1685ad0cf 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt @@ -45,8 +45,11 @@ internal object AmountStatePreviewData { isFiatUnavailable = false, isError = false, error = TextReference.EMPTY, + isValuePasted = false, + onValuePastedTriggerDismiss = {}, ), isSegmentedButtonsEnabled = true, + selectedButton = 0, ) val fiatAmountState = amountState.copy( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt index 10a3142e61..0fedb031f2 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt @@ -14,7 +14,9 @@ internal object SendClickIntentsStub : SendClickIntents { override fun onBackClick() {} - override fun onNextClick() {} + override fun onCloseClick() {} + + override fun onNextClick(isFromEdit: Boolean) {} override fun onPrevClick() {} @@ -30,6 +32,8 @@ internal object SendClickIntentsStub : SendClickIntents { override fun onMaxValueClick() {} + override fun onAmountPasteTriggerDismiss() {} + override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {} override fun onRecipientMemoValueChange(value: String) {} @@ -56,7 +60,11 @@ internal object SendClickIntentsStub : SendClickIntents { override fun onShareClick() {} - override fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class) {} + override fun onAmountReduceClick( + reduceAmountBy: BigDecimal?, + reduceAmountTo: BigDecimal?, + clazz: Class, + ) {} override fun onNotificationCancel(clazz: Class) {} } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 604a9ba0a6..e858d9b150 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -1,13 +1,14 @@ package com.tangem.features.send.impl.presentation.ui import androidx.compose.animation.* +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -18,11 +19,13 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.R +import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SecondaryButtonIconStart import com.tangem.core.ui.components.SpacerW12 import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter @@ -40,6 +43,7 @@ internal fun SendNavigationButtons( val isSuccess = sendState.isSuccess val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess val isSentState = currentState.type == SendUiStateType.Send && isSuccess + Column( modifier = modifier .padding( @@ -48,7 +52,11 @@ internal fun SendNavigationButtons( bottom = TangemTheme.dimens.spacing16, ), ) { - SendingText(uiState = uiState, isVisible = isSendingState) + SendingText( + uiState = uiState, + isEditState = currentState.isFromConfirmation, + isVisible = isSendingState, + ) SendDoneButtons( txUrl = sendState.txUrl, onExploreClick = uiState.clickIntents::onExploreClick, @@ -91,25 +99,26 @@ private fun SendNavigationButton( } else { TangemButtonIconPosition.None } - Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - modifier = modifier, - ) { + + Row(modifier = modifier) { AnimatedVisibility( visible = !isEditingDisabled && isCorrectScreen && !isFromConfirmation, enter = expandHorizontally(expandFrom = Alignment.End), exit = shrinkHorizontally(shrinkTowards = Alignment.End), ) { - Icon( - painter = painterResource(R.drawable.ic_back_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.button.secondary) - .clickable { uiState.clickIntents.onPrevClick() } - .padding(TangemTheme.dimens.spacing12), - ) + Row { + Icon( + painter = painterResource(R.drawable.ic_back_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.button.secondary) + .clickable { uiState.clickIntents.onPrevClick() } + .padding(TangemTheme.dimens.spacing12), + ) + SpacerW12() + } } TangemButton( text = stringResource(buttonTextId), @@ -127,20 +136,40 @@ private fun SendNavigationButton( } @Composable -private fun SendingText(uiState: SendUiState, isVisible: Boolean, modifier: Modifier = Modifier) { +private fun SendingText( + uiState: SendUiState, + isEditState: Boolean, + isVisible: Boolean, + modifier: Modifier = Modifier, +) { + var isVisibleProxy by remember { mutableStateOf(isVisible) } + val keyboard by keyboardAsState() + + // the text should appear when the keyboard is closed + LaunchedEffect(isVisible, keyboard) { + if (isVisible && keyboard is Keyboard.Opened) { + return@LaunchedEffect + } + isVisibleProxy = isVisible + } + AnimatedVisibility( - visible = isVisible, + visible = isVisibleProxy, modifier = modifier, - enter = slideInVertically().plus(fadeIn()), - exit = slideOutVertically().plus(fadeOut()), + enter = slideInVertically() + fadeIn(), + exit = fadeOut(tween(durationMillis = 300)), label = "Animate show sending state text", ) { - val amountState = uiState.amountState - val feeState = uiState.feeState + val amountState = uiState.getAmountState(isEditState) + val feeState = uiState.getFeeState(isEditState) val fiatRate = feeState?.rate val fiatAmount = amountState?.amountTextField?.fiatAmount val feeFiat = fiatRate?.let { feeState.fee?.amount?.value?.multiply(it) } - val sendingFiat = feeFiat?.let { fiatAmount?.value?.plus(it) } + val sendingFiat = if (uiState.isSubtracted) { + fiatAmount?.value + } else { + feeFiat?.let { fiatAmount?.value?.plus(it) } + } if (feeFiat != null && sendingFiat != null) { val sendingValue = BigDecimalFormatter.formatFiatAmount( @@ -217,11 +246,11 @@ private fun getButtonData( SendUiStateType.Amount, SendUiStateType.Recipient, SendUiStateType.Fee, - -> if (currentState.isFromConfirmation) { - R.string.common_continue to uiState.clickIntents::onNextClick - } else { - R.string.common_next to uiState.clickIntents::onNextClick - } + -> R.string.common_next to { uiState.clickIntents.onNextClick() } + SendUiStateType.EditFee, + SendUiStateType.EditAmount, + SendUiStateType.EditRecipient, + -> R.string.common_continue to { uiState.clickIntents.onNextClick(isFromEdit = true) } SendUiStateType.Send -> when { isSuccess -> R.string.common_close isSending -> R.string.send_sending @@ -232,10 +261,13 @@ private fun getButtonData( private fun isButtonEnabled(currentState: SendUiCurrentScreen, uiState: SendUiState): Boolean { return when (currentState.type) { - SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false - SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled ?: false - SendUiStateType.Fee -> uiState.feeState?.isPrimaryButtonEnabled ?: false - SendUiStateType.Send -> uiState.sendState?.isPrimaryButtonEnabled ?: false + SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled + SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled + SendUiStateType.Fee -> uiState.feeState?.isPrimaryButtonEnabled + SendUiStateType.Send -> uiState.sendState?.isPrimaryButtonEnabled + SendUiStateType.EditAmount -> uiState.editAmountState?.isPrimaryButtonEnabled + SendUiStateType.EditRecipient -> uiState.editRecipientState?.isPrimaryButtonEnabled + SendUiStateType.EditFee -> uiState.editFeeState?.isPrimaryButtonEnabled else -> true - } + } ?: false } \ 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 caf0a2f4f2..d6529709b7 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 @@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.ui import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.tween import androidx.compose.animation.togetherWith import androidx.compose.foundation.background @@ -24,12 +25,15 @@ import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent import com.tangem.features.send.impl.presentation.ui.send.SendContent +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.withIndex @Composable internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) { val snackbarHostState = remember { SnackbarHostState() } - val sendState = uiState.sendState ?: return - BackHandler { uiState.clickIntents.onBackClick() } + BackHandler(onBack = uiState.clickIntents::onBackClick) Column( modifier = Modifier .fillMaxSize() @@ -38,38 +42,9 @@ internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) .background(color = TangemTheme.colors.background.tertiary), horizontalAlignment = Alignment.CenterHorizontally, ) { - val titleRes = when (currentState.type) { - SendUiStateType.Amount -> resourceReference(R.string.send_amount_label) - SendUiStateType.Recipient -> resourceReference(R.string.send_recipient_label) - SendUiStateType.Fee -> resourceReference(R.string.common_fee_selector_title) - SendUiStateType.Send -> if (!sendState.isSuccess) { - resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencyName)) - } else { - null - } - else -> null - } - val isSending = currentState.type == SendUiStateType.Send && !uiState.sendState.isSuccess - val subtitleRes = if (isSending) { - uiState.amountState?.walletName - } else { - null - } - val iconRes = if (currentState.type == SendUiStateType.Recipient) { - R.drawable.ic_qrcode_scan_24 - } else { - null - } - - AppBarWithBackButtonAndIcon( - text = titleRes?.resolveReference(), - subtitle = subtitleRes, - onBackClick = uiState.clickIntents::popBackStack, - onIconClick = uiState.clickIntents::onQrCodeScanClick, - backIconRes = R.drawable.ic_close_24, - iconRes = iconRes, - backgroundColor = TangemTheme.colors.background.tertiary, - modifier = Modifier.height(TangemTheme.dimens.size56), + SendAppBar( + uiState = uiState, + currentState = currentState, ) SendScreenContent( uiState = uiState, @@ -89,26 +64,90 @@ internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) } @Composable -private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentScreen, modifier: Modifier = Modifier) { - var lastState by remember { mutableIntStateOf(currentState.type.ordinal) } - val direction = remember(currentState.type.ordinal) { - if (lastState < currentState.type.ordinal) { - AnimatedContentTransitionScope.SlideDirection.Start +private fun SendAppBar(uiState: SendUiState, currentState: SendUiCurrentScreen) { + val (titleRes, subtitleRes) = when (currentState.type) { + SendUiStateType.Amount, + SendUiStateType.EditAmount, + -> resourceReference(R.string.send_amount_label) to null + SendUiStateType.Recipient, + SendUiStateType.EditRecipient, + -> resourceReference(R.string.send_recipient_label) to null + SendUiStateType.Fee, + SendUiStateType.EditFee, + -> resourceReference(R.string.common_fee_selector_title) to null + SendUiStateType.Send -> if (uiState.sendState?.isSuccess == false) { + resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencyName)) to + uiState.amountState?.walletName } else { - AnimatedContentTransitionScope.SlideDirection.End + null to null } + else -> null to null } + val iconRes = if (currentState.type == SendUiStateType.Recipient) { + R.drawable.ic_qrcode_scan_24 + } else { + null + } + val backIcon = when (currentState.type) { + SendUiStateType.EditAmount, + SendUiStateType.EditFee, + SendUiStateType.EditRecipient, + -> R.drawable.ic_back_24 + else -> R.drawable.ic_close_24 + } + AppBarWithBackButtonAndIcon( + text = titleRes?.resolveReference(), + subtitle = subtitleRes, + onBackClick = uiState.clickIntents::onCloseClick, + onIconClick = uiState.clickIntents::onQrCodeScanClick, + backIconRes = backIcon, + iconRes = iconRes, + backgroundColor = TangemTheme.colors.background.tertiary, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentScreen, modifier: Modifier = Modifier) { + var currentStateProxy by remember { mutableStateOf(currentState) } + var isTransitionAnimationRunning by remember { mutableStateOf(false) } + + // Prevent quick screen changes to avoid some of the transition animation distortions + LaunchedEffect(currentState) { + snapshotFlow { isTransitionAnimationRunning } + .withIndex() + .map { (index, running) -> + if (running && index != 0) { + delay(timeMillis = 200) + } + running + } + .first { !it } + + currentStateProxy = currentState + } + // Restrict pressing the back button while screen transition is running to avoid most of the animation distortions + BackHandler(enabled = isTransitionAnimationRunning) {} + // Box is needed to fix animation with resizing of AnimatedContent - Box(modifier = modifier) { + Box(modifier = modifier.fillMaxSize()) { AnimatedContent( - targetState = currentState, + targetState = currentStateProxy, + contentAlignment = Alignment.TopCenter, label = "Send Scree Navigation", transitionSpec = { - lastState = currentState.type.ordinal + val direction = if (initialState.type.ordinal < targetState.type.ordinal) { + AnimatedContentTransitionScope.SlideDirection.Start + } else { + AnimatedContentTransitionScope.SlideDirection.End + } + slideIntoContainer(towards = direction, animationSpec = tween()) .togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween())) }, ) { state -> + isTransitionAnimationRunning = transition.targetState != transition.currentState when (state.type) { SendUiStateType.Amount -> SendAmountContent( @@ -116,13 +155,23 @@ private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentS isBalanceHiding = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, ) + SendUiStateType.EditAmount -> SendAmountContent( + amountState = uiState.editAmountState, + isBalanceHiding = uiState.isBalanceHidden, + clickIntents = uiState.clickIntents, + ) SendUiStateType.Recipient -> SendRecipientContent( uiState = uiState.recipientState, clickIntents = uiState.clickIntents, isBalanceHidden = uiState.isBalanceHidden, ) - SendUiStateType.Fee -> SendSpeedAndFeeContent( - state = uiState.feeState, + SendUiStateType.EditRecipient -> SendRecipientContent( + uiState = uiState.editRecipientState, + clickIntents = uiState.clickIntents, + isBalanceHidden = uiState.isBalanceHidden, + ) + SendUiStateType.EditFee -> SendSpeedAndFeeContent( + state = uiState.editFeeState, clickIntents = uiState.clickIntents, ) SendUiStateType.Send -> SendContent(uiState) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt index 340f8e20ad..1b6315e1ea 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt @@ -29,6 +29,7 @@ internal fun LazyListScope.buttons( segmentedButtonConfig: PersistentList, clickIntents: SendClickIntents, isSegmentedButtonsEnabled: Boolean, + selectedButton: Int, ) { item( key = AMOUNT_BUTTONS_KEY, @@ -48,6 +49,7 @@ internal fun LazyListScope.buttons( hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) clickIntents.onCurrencyChangeClick(it.isFiat) }, + initialSelectedItem = segmentedButtonConfig.getOrNull(selectedButton), isEnabled = isSegmentedButtonsEnabled, ) { SendAmountCurrencyButton( 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 f92a93cdae..8f7a69c8c1 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 @@ -22,7 +22,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.features.send.impl.presentation.state.fields.SendTextField -import kotlinx.coroutines.job +import kotlinx.coroutines.delay @Composable internal fun AmountField(sendField: SendTextField.AmountField, appCurrencyCode: String) { @@ -53,6 +53,8 @@ internal fun AmountField(sendField: SendTextField.AmountField, appCurrencyCode: textAlign = TextAlign.Center, ), isAutoResize = true, + isValuePasted = sendField.isValuePasted, + onValuePastedTriggerDismiss = sendField.onValuePastedTriggerDismiss, modifier = Modifier .focusRequester(requester) .padding( @@ -64,9 +66,8 @@ internal fun AmountField(sendField: SendTextField.AmountField, appCurrencyCode: ) LaunchedEffect(key1 = Unit) { - this.coroutineContext.job.invokeOnCompletion { - requester.requestFocus() - } + delay(timeMillis = 200) + requester.requestFocus() } AmountSecondary(sendField, appCurrencyCode) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt index 6806468b2b..01d23d4edb 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt @@ -21,6 +21,7 @@ internal fun SendAmountContent( clickIntents: SendClickIntents, ) { if (amountState == null) return + // Do not put fillMaxSize() in here LazyColumn( modifier = Modifier .padding( @@ -35,6 +36,7 @@ internal fun SendAmountContent( segmentedButtonConfig = amountState.segmentedButtonConfig, clickIntents = clickIntents, isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled, + selectedButton = amountState.selectedButton, ) } } 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 88f4a37a85..4b6919dd3e 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 @@ -2,7 +2,6 @@ package com.tangem.features.send.impl.presentation.ui.fee import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -27,8 +26,7 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S val isCustomSelected = feeSendState?.selectedFee == FeeType.Custom val hasNotifications = notifications.isNotEmpty() LazyColumn( - modifier = Modifier - .fillMaxSize() + modifier = Modifier // Do not put fillMaxSize() in here .background(TangemTheme.colors.background.tertiary) .padding( start = TangemTheme.dimens.spacing16, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt index 0a2a65c93e..d9c609a5ca 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -4,15 +4,14 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.* import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.tangem.blockchain.common.Amount 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.rows.SelectorRowItem import com.tangem.core.ui.res.TangemTheme @@ -54,30 +53,55 @@ internal fun SendSpeedSelectorItem( onSelect = onSelect, modifier = modifier, preDot = getCryptoReference(amount, state.isFeeApproximate), - postDot = getFiatReference(amount, state.rate, state.appCurrency), + postDot = getFiatReference(amount?.value, state.rate, state.appCurrency), ellipsizeOffset = amount?.currencySymbol?.length, isSelected = content?.selectedFee == feeType, showDivider = showDivider, ) - SendSpeedSelectorItemError(isError = feeSelectorState is FeeSelectorState.Error) + FeeLoading(feeSelectorState) + FeeError(feeSelectorState) } } } @Composable -private fun SendSpeedSelectorItemError(isError: Boolean) { +private fun FeeLoading(feeSelectorState: FeeSelectorState) { Row { SpacerWMax() AnimatedVisibility( - visible = isError, - label = "Error state indication animation", - enter = fadeIn(), - exit = fadeOut(), + visible = feeSelectorState == FeeSelectorState.Loading, + label = "Fee Loading State Change", + modifier = Modifier.align(Alignment.CenterVertically), + ) { + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier + .padding( + vertical = TangemTheme.dimens.spacing18, + horizontal = TangemTheme.dimens.spacing12, + ) + .size( + height = TangemTheme.dimens.size12, + width = TangemTheme.dimens.size90, + ), + ) + } + } +} + +@Composable +private fun FeeError(feeSelectorState: FeeSelectorState) { + Row { + SpacerWMax() + AnimatedVisibility( + visible = feeSelectorState == FeeSelectorState.Error, + label = "Fee Error State Change", + modifier = Modifier.align(Alignment.CenterVertically), ) { Text( text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, - style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body2, modifier = Modifier .padding( vertical = TangemTheme.dimens.spacing14, @@ -101,10 +125,9 @@ private fun FeeSelectorState.Content.getAmount(feeType: FeeType): Amount? { private fun FeeSelectorState.Content?.getDividerAndVisibility(feeType: FeeType): Pair { val hasCustomValues = !this?.customValues.isNullOrEmpty() val isNotSingle = this?.fees !is TransactionFee.Single - val isLoaded = this?.fees != null return when (feeType) { FeeType.Slow -> true to isNotSingle - FeeType.Market -> isNotSingle to isLoaded + FeeType.Market -> isNotSingle to true FeeType.Fast -> hasCustomValues to isNotSingle FeeType.Custom -> false to hasCustomValues } 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 f728ff97cb..e37a0ced3d 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 @@ -4,7 +4,6 @@ import androidx.annotation.StringRes import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -48,8 +47,7 @@ internal fun SendRecipientContent( val isValidating by remember(uiState.isValidating) { derivedStateOf { uiState.isValidating } } val isError by remember(address.isError) { derivedStateOf { address.isError } } LazyColumn( - modifier = Modifier - .fillMaxSize() + modifier = Modifier // Do not put fillMaxSize() in here .background(TangemTheme.colors.background.tertiary) .padding( start = TangemTheme.dimens.spacing16, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt index 85ff055ed6..ab68759908 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt @@ -31,8 +31,8 @@ internal fun AmountBlock( val amount = amountState.amountTextField val cryptoAmount = BigDecimalFormatter.formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol) - val fiatAmount = BigDecimalFormatter.formatFiatEditableAmount( - fiatAmount = amount.fiatValue, + val fiatAmount = BigDecimalFormatter.formatFiatAmount( + fiatAmount = amount.fiatAmount.value, fiatCurrencySymbol = amount.fiatAmount.currencySymbol, fiatCurrencyCode = amountState.appCurrencyCode, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index 11423e7c73..54ef73296a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -32,7 +32,7 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) - .clickable(enabled = !isSuccess && feeState.fee != null) { onClick() } + .clickable(enabled = !isSuccess, onClick = onClick) .padding(TangemTheme.dimens.spacing12), ) { Text( @@ -60,7 +60,7 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick titleRes = title, iconRes = icon, preDot = getCryptoReference(feeAmount, feeState.isFeeApproximate), - postDot = feeAmount?.let { getFiatReference(it, feeState.rate, feeState.appCurrency) }, + postDot = getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency), ellipsizeOffset = feeAmount?.currencySymbol?.length, isSelected = true, showDivider = false, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt index 0801e63cd6..e3b92db56c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt @@ -35,6 +35,7 @@ internal fun RecipientBlock( Column( modifier = Modifier + .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) .background(backgroundColor) .clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick) 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 78809756c4..6a6cc0550d 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 @@ -4,7 +4,10 @@ import androidx.compose.animation.* import androidx.compose.animation.core.MutableTransitionState import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.Icon @@ -33,9 +36,7 @@ private const val TAP_HELP_ANIMATION_DELAY = 500L internal fun SendContent(uiState: SendUiState) { val sendState = uiState.sendState ?: return LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = TangemTheme.dimens.spacing16), + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), ) { blocks(uiState) tapHelp(isDisplay = sendState.showTapHelp) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt index 022d0c5711..73ef044814 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt @@ -5,8 +5,13 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.BigDecimalFormatter.EMPTY_BALANCE_SIGN import com.tangem.domain.appcurrency.model.AppCurrency import java.math.BigDecimal +import java.math.RoundingMode + +private const val FIAT_DECIMALS = 2 +private const val FEE_MINIMUM_VALUE = 0.01 internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { if (amount == null) return null @@ -22,13 +27,33 @@ internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): Tex ) } -internal fun getFiatReference(amount: Amount?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? { - if (amount == null) return null - return stringReference( +internal fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? { + if (value == null || rate == null) return null + val formattedFiat = getFiatString(value = value, rate = rate, appCurrency = appCurrency) + return stringReference(formattedFiat) +} + +internal fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String { + if (value == null || rate == null) return EMPTY_BALANCE_SIGN + val feeValue = value.multiply(rate) + val scaled = feeValue.setScale(FIAT_DECIMALS, RoundingMode.UP) ?: BigDecimal.ZERO + val formattedValue = if (scaled < BigDecimal(FEE_MINIMUM_VALUE)) { + buildString { + append(BigDecimalFormatter.CAN_BE_LOWER_SIGN) + append( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = BigDecimal(FEE_MINIMUM_VALUE), + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ) + } + } else { BigDecimalFormatter.formatFiatAmount( - fiatAmount = rate?.let { amount.value?.multiply(it) }, + fiatAmount = feeValue, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, - ), - ) + ) + } + return formattedValue } \ No newline at end of file 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 74e5d81128..32ba81ce73 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 @@ -14,7 +14,9 @@ internal interface SendClickIntents { fun onBackClick() - fun onNextClick() + fun onCloseClick() + + fun onNextClick(isFromEdit: Boolean = false) fun onPrevClick() @@ -30,6 +32,8 @@ internal interface SendClickIntents { fun onCurrencyChangeClick(isFiat: Boolean) fun onMaxValueClick() + + fun onAmountPasteTriggerDismiss() // endregion // region Recipient @@ -63,7 +67,11 @@ internal interface SendClickIntents { fun onShareClick() - fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class) + fun onAmountReduceClick( + reduceAmountBy: BigDecimal? = null, + reduceAmountTo: BigDecimal? = null, + clazz: Class, + ) fun onNotificationCancel(clazz: Class) // endregion 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 caec97e044..5410b54f17 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 @@ -15,6 +15,8 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder @@ -45,7 +47,7 @@ import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents import com.tangem.features.send.impl.presentation.analytics.SendScreenSource -import com.tangem.features.send.impl.presentation.analytics.utils.SendOnNextScreenAnalyticSender +import com.tangem.features.send.impl.presentation.analytics.utils.SendScreenAnalyticSender 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.amount.AmountStateFactory @@ -92,6 +94,7 @@ internal class SendViewModel @Inject constructor( private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase, private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, + private val listenToQrScanningUseCase: ListenToQrScanningUseCase, currencyChecksRepository: CurrencyChecksRepository, isFeeApproximateUseCase: IsFeeApproximateUseCase, validateWalletMemoUseCase: ValidateWalletMemoUseCase, @@ -119,6 +122,7 @@ internal class SendViewModel @Inject constructor( private val stateFactory = SendStateFactory( clickIntents = this, + stateRouterProvider = Provider { stateRouter }, currentStateProvider = Provider { uiState }, userWalletProvider = Provider { userWallet }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), @@ -129,12 +133,14 @@ internal class SendViewModel @Inject constructor( ) private val amountStateFactory = AmountStateFactory( + stateRouterProvider = Provider { stateRouter }, currentStateProvider = Provider { uiState }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, ) private val feeStateFactory = FeeStateFactory( clickIntents = this, + stateRouterProvider = Provider { stateRouter }, currentStateProvider = Provider { uiState }, feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), @@ -143,6 +149,7 @@ internal class SendViewModel @Inject constructor( private val eventStateFactory = SendEventStateFactory( clickIntents = this, + stateRouterProvider = Provider { stateRouter }, currentStateProvider = Provider { uiState }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, feeStateFactory = feeStateFactory, @@ -161,14 +168,19 @@ internal class SendViewModel @Inject constructor( userWalletProvider = Provider { userWallet }, stateRouterProvider = Provider { stateRouter }, isSubtractAvailableProvider = Provider { isAmountSubtractAvailable }, + appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), currencyChecksRepository = currencyChecksRepository, clickIntents = this, analyticsEventHandler = analyticsEventHandler, getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase, ) - private val sendOnNextScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) { - SendOnNextScreenAnalyticSender(analyticsEventHandler) + private val sendScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) { + SendScreenAnalyticSender( + stateRouterProvider = Provider { stateRouter }, + currentStateProvider = Provider { uiState }, + analyticsEventHandler = analyticsEventHandler, + ) } // todo convert to StateFlow @@ -189,11 +201,11 @@ internal class SendViewModel @Inject constructor( private var memoValidationJobHolder = JobHolder() private var sendNotificationsJobHolder = JobHolder() private var feeNotificationsJobHolder = JobHolder() - private var qrScannerJobHolder = JobHolder() private var sendIdleTimer = 0L init { + subscribeOnQRScannerResult() subscribeOnCurrencyStatusUpdates() subscribeOnBalanceHidden() getTapHelpPreviewAvailability() @@ -215,6 +227,13 @@ internal class SendViewModel @Inject constructor( this.stateRouter = stateRouter } + private fun subscribeOnQRScannerResult() { + listenToQrScanningUseCase(SourceType.SEND) + .getOrElse { emptyFlow() } + .onEach(::onQrCodeScanned) + .launchIn(viewModelScope) + } + private fun subscribeOnCurrencyStatusUpdates() { viewModelScope.launch(dispatchers.main) { getUserWalletUseCase(userWalletId).fold( @@ -444,8 +463,13 @@ internal class SendViewModel @Inject constructor( stateRouter.currentState .onEach { when (it.type) { - SendUiStateType.Fee -> loadFee() - SendUiStateType.Send -> sendIdleTimer = SystemClock.elapsedRealtime() + SendUiStateType.Fee, + SendUiStateType.EditFee, + -> loadFee() + SendUiStateType.Send -> { + uiState = stateFactory.getIsAmountSubtractedState(isAmountSubtractAvailable) + sendIdleTimer = SystemClock.elapsedRealtime() + } else -> Unit } } @@ -479,16 +503,28 @@ internal class SendViewModel @Inject constructor( stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true) } - override fun onNextClick() { + override fun onCloseClick() { + sendScreenAnalyticSender.sendOnClose() + when (stateRouter.currentState.value.type) { + SendUiStateType.EditAmount, + SendUiStateType.EditFee, + SendUiStateType.EditRecipient, + -> onBackClick() + else -> popBackStack() + } + } + + override fun onNextClick(isFromEdit: Boolean) { val currentState = stateRouter.currentState.value - sendOnNextScreenAnalyticSender.send(currentState.type, uiState) + uiState = stateFactory.syncEditStates(isFromEdit = isFromEdit) + sendScreenAnalyticSender.send(currentState.type, uiState) when (currentState.type) { - SendUiStateType.Fee -> { - if (onFeeNext()) return - } - SendUiStateType.Amount -> { - loadFee() - } + SendUiStateType.Fee, + SendUiStateType.EditFee, + -> if (onFeeNext()) return + SendUiStateType.Amount, + SendUiStateType.EditAmount, + -> loadFee() else -> Unit } @@ -513,13 +549,14 @@ internal class SendViewModel @Inject constructor( innerRouter.openTokenDetails(userWalletId, currency) private fun onFeeNext(): Boolean { - if (checkIfFeeTooLow(uiState)) { + val feeState = uiState.getFeeState(stateRouter.isEditState) + val feeSelectorState = feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false + if (checkIfFeeTooLow(feeSelectorState)) { uiState = eventStateFactory.getFeeTooLowAlert( onConsume = { uiState = eventStateFactory.onConsumeEventState() }, ) return true } - val feeSelectorState = uiState.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false return checkIfFeeTooHigh( feeSelectorState = feeSelectorState, onShow = { diff -> @@ -536,6 +573,23 @@ internal class SendViewModel @Inject constructor( feeJobHolder.cancel() } } + + private fun onQrCodeScanned(address: String) { + parseQrCodeUseCase(address, cryptoCurrency).fold( + ifRight = { parsedCode -> + onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode) + parsedCode.amount?.let { + onAmountValueChange(it.parseBigDecimal(decimals = cryptoCurrency.decimals)) + } + parsedCode.memo?.let { onRecipientMemoValueChange(it) } + }, + ifLeft = { + onRecipientAddressValueChange(address, EnterAddressSource.QRCode) + Timber.w(it) + }, + ) + } + // endregion // region amount state clicks @@ -551,29 +605,16 @@ internal class SendViewModel @Inject constructor( uiState = amountStateFactory.getOnMaxAmountClick() analyticsEventHandler.send(SendAnalyticEvents.MaxAmountButtonClicked) } - // endregion - // region recipient state clicks - fun onQrCodeScanned(address: String) { - viewModelScope.launch(dispatchers.main) { - parseQrCodeUseCase(address, cryptoCurrency).fold( - ifRight = { parsedCode -> - onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode) - parsedCode.amount?.let { - onAmountValueChange(it.parseBigDecimal(decimals = cryptoCurrency.decimals)) - } - parsedCode.memo?.let { onRecipientMemoValueChange(it) } - }, - ifLeft = { - onRecipientAddressValueChange(address, EnterAddressSource.QRCode) - Timber.w(it) - }, - ) - }.saveIn(qrScannerJobHolder) + override fun onAmountPasteTriggerDismiss() { + uiState = amountStateFactory.getOnAmountPastedTriggerDismiss() } +// endregion + +// region recipient state clicks override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) { - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { if (!checkIfXrpAddressValue(value)) { uiState = stateFactory.onRecipientAddressValueChange(value) uiState = stateFactory.getOnRecipientAddressValidationStarted() @@ -586,7 +627,7 @@ internal class SendViewModel @Inject constructor( } override fun onRecipientMemoValueChange(value: String) { - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { if (!checkIfXrpAddressValue(value)) { uiState = stateFactory.getOnRecipientMemoValueChange(value) uiState = stateFactory.getOnRecipientAddressValidationStarted() @@ -627,9 +668,9 @@ internal class SendViewModel @Inject constructor( private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) { val isRecent = type == EnterAddressSource.RecentAddress - if (isRecent && isValidAddress) onNextClick() + if (isRecent && isValidAddress) onNextClick(stateRouter.isEditState) } - // endregion +// endregion // region fee override fun feeReload() = loadFee() @@ -662,10 +703,12 @@ internal class SendViewModel @Inject constructor( val isShowStatus = uiState.feeState?.fee == null if (isShowStatus) { uiState = feeStateFactory.onFeeOnLoadingState() + updateNotifications() } val result = callFeeUseCase()?.fold( ifRight = { uiState = feeStateFactory.onFeeOnLoadedState(it) + sendIdleTimer = SystemClock.elapsedRealtime() }, ifLeft = { onFeeLoadFailed(isShowStatus) @@ -677,9 +720,6 @@ internal class SendViewModel @Inject constructor( updateNotifications() updateFeeNotifications() }.saveIn(feeJobHolder) - .invokeOnCompletion { - // todo - } } private fun onFeeLoadFailed(isShowStatus: Boolean) { @@ -694,18 +734,19 @@ internal class SendViewModel @Inject constructor( } private suspend fun callFeeUseCase(): Either? { - val amountState = uiState.amountState ?: return null - val recipientState = uiState.recipientState ?: return null + val isFromConfirmation = stateRouter.currentState.value.isFromConfirmation + val amountState = uiState.getAmountState(isFromConfirmation) ?: return null + val recipientState = uiState.getRecipientState(isFromConfirmation) ?: return null val amount = amountState.amountTextField.cryptoAmount.value ?: return null return getFeeUseCase.invoke( amount = amount, destination = recipientState.addressTextField.value, userWallet = userWallet, - cryptoCurrency = cryptoCurrency, + cryptoCurrency = cryptoCurrencyStatus.currency, ) } - // endregion +// endregion // region send state clicks override fun onSendClick() { @@ -722,12 +763,14 @@ internal class SendViewModel @Inject constructor( } override fun showAmount() { + uiState = stateFactory.syncEditStates(isFromEdit = false) stateRouter.showAmount(isFromConfirmation = true) setNeverToShowTapHelp() analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount)) } override fun showRecipient() { + uiState = stateFactory.syncEditStates(isFromEdit = false) stateRouter.showRecipient(isFromConfirmation = true) uiState = stateFactory.getHiddenTapHelpState() setNeverToShowTapHelp() @@ -735,6 +778,7 @@ internal class SendViewModel @Inject constructor( } override fun showFee() { + uiState = stateFactory.syncEditStates(isFromEdit = false) stateRouter.showFee(isFromConfirmation = true) setNeverToShowTapHelp() analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee)) @@ -754,8 +798,17 @@ internal class SendViewModel @Inject constructor( analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked) } - override fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class) { - uiState = amountStateFactory.getOnAmountValueChange(reducedAmount.parseBigDecimal(cryptoCurrency.decimals)) + override fun onAmountReduceClick( + reduceAmountBy: BigDecimal?, + reduceAmountTo: BigDecimal?, + clazz: Class, + ) { + uiState = when { + reduceAmountBy != null -> amountStateFactory.getOnAmountReduceByState(reduceAmountBy) + reduceAmountTo != null -> amountStateFactory.getOnAmountReduceToState(reduceAmountTo) + else -> return + } + uiState = sendNotificationFactory.dismissNotificationState(clazz) feeReload() } @@ -777,6 +830,7 @@ internal class SendViewModel @Inject constructor( cryptoCurrencyStatus = cryptoCurrencyStatus, amountValue = amountValue, feeValue = feeValue, + reduceAmountBy = uiState.sendState?.reduceAmountBy, ) viewModelScope.launch(dispatchers.main) { @@ -890,7 +944,7 @@ internal class SendViewModel @Inject constructor( } uiState = stateFactory.getHiddenTapHelpState() } - // endregion +// endregion private companion object { const val CHECK_FEE_UPDATE_DELAY = 60_000L diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt index d3fd7a6127..d31a165d9a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt @@ -35,7 +35,7 @@ internal class TxHistoryItemStateConverter( title = item.extractTitle(), subtitle = item.extractSubtitle(), timestamp = item.timestampInMillis, - onClick = { clickIntents.onVisaTransactionClick(item.txHash) }, + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 40649092a8..99fc8ec14e 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -86,9 +86,9 @@ room = "2.6.1" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-609" +tangemBlockchainSdk = "develop-620" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-346" +tangemCardSdk = "develop-351" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 502c011f5f..1ae43f5950 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -336,6 +336,10 @@ private val excludedBlockchains = listOf( Blockchain.Nexa, Blockchain.NexaTestnet, Blockchain.Radiant, + Blockchain.Manta, + Blockchain.MantaTestnet, + Blockchain.Mantle, + Blockchain.MantleTestnet, Blockchain.Koinos, Blockchain.KoinosTestnet, ) \ No newline at end of file