Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-05 22:02:01 +01:00
commit b96f007568
69 changed files with 1367 additions and 564 deletions

@ -1 +1 @@
Subproject commit fdf0660901442ade296db42bb867fc5c16a14262
Subproject commit 07cfce37ff84e70081aca82d9948acb13c5c07b0

View file

@ -33,7 +33,9 @@ class ResetToFactorySettingsTask(
}
private fun resetBackup(session: CardSession, callback: (result: CompletionResult<Card>) -> 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
}

View file

@ -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)

View file

@ -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"

View file

@ -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<Int, Token>() {
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, Token>): 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) },
)

View file

@ -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)

View file

@ -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<String, AnalyticsHandler>()
private val paramsInterceptors = mutableMapOf<String, ParamsInterceptor>()
private val analyticsFilters = mutableSetOf<AnalyticsEventFilter>()
private val analyticsMutex = Mutex()
private val analyticsHandlers: List<AnalyticsHandler>
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<String, String> {
private suspend fun applyParamsInterceptors(event: AnalyticsEvent): MutableMap<String, String> {
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
}

View file

@ -13,7 +13,7 @@
},
{
"name": "LOCAL_USER_LOGS_ENABLED",
"version": "5.8.0"
"version": "5.10.0"
},
{
"name": "GENERATE_XPUB_ENABLED",

View file

@ -4,12 +4,16 @@
<string name="add_tokens_title">Валюты</string>
<string name="address_qr_code_message_format">Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств.</string>
<string name="alert_button_request_support">Обратиться в поддержку</string>
<string name="alert_button_try_again">Попробовать снова</string>
<string name="alert_demo_feature_disabled">Эта функция недоступна в демонстрационном режиме</string>
<string name="alert_failed_to_send_transaction_message">Причина: %s</string>
<string name="alert_failed_to_send_transaction_title">Не могу отправить транзакцию</string>
<string name="alert_manage_tokens_unsupported_blockchain_by_card_message">Выбранный кошелёк не поддерживает сеть %1$s</string>
<string name="alert_manage_tokens_unsupported_curve_message">Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен.</string>
<string name="alert_manage_tokens_unsupported_message">Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки.</string>
<string name="alert_negative_app_rate_sent_message">Спасибо за ваш отзыв. Мы ответим в кратчайшие сроки.</string>
<string name="alert_negative_app_rate_sent_title">Ваши предложения отправлены</string>
<string name="alert_troubleshooting_scan_card_message">Пожалуйста, попробуйте приложить карту в точности, как показано на анимации, или запросите поддержку.</string>
<string name="alert_troubleshooting_scan_card_title">У вас возникли трудности со сканированием карты?</string>
<string name="alert_unsupported_card">Эта карта не предназначена для работы с этим приложением</string>
<string name="app_settings_default_fee_footer">Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться.</string>
@ -24,6 +28,8 @@
<string name="app_settings_theme_mode_dark">Тёмная</string>
<string name="app_settings_theme_mode_light">Светлая</string>
<string name="app_settings_theme_mode_system">Как в системе</string>
<string name="app_settings_theme_selection_footer">При выборе настройки как в системе приложение будет использовать тему в соответствии с настройками вашего устройства</string>
<string name="app_settings_theme_selection_system_short">Системная</string>
<string name="app_settings_theme_selector_title">Тема</string>
<string name="app_settings_title">Настройки приложения</string>
<string name="balance_hidden_description">Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\"</string>
@ -75,6 +81,7 @@
<string name="common_create">Создать</string>
<string name="common_delete">Удалить</string>
<string name="common_disabled">Отключено</string>
<string name="common_disconnect">Отключить</string>
<string name="common_done">Готово</string>
<string name="common_enable">Включить</string>
<string name="common_enabled">Включено</string>
@ -107,6 +114,7 @@
<string name="common_reject">Отклонить</string>
<string name="common_reload">Перезагрузить</string>
<string name="common_rename">Переименовать</string>
<string name="common_retry">Повторить</string>
<string name="common_save_changes">Сохранить изменения</string>
<string name="common_search">Искать</string>
<string name="common_search_tokens">Поиск токенов</string>
@ -130,6 +138,7 @@
<string name="common_understand">Я понял</string>
<string name="common_unknown_error">Произошла ошибка. Пожалуйста, попробуйте снова.</string>
<string name="common_unreachable">Недоступно</string>
<string name="common_warning">Предупреждение</string>
<string name="common_yes">Да</string>
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
<string name="currency_subtitle_expanded">Доступные сети</string>
@ -176,6 +185,7 @@
<string name="details_row_title_flip_to_hide">Скрывать балансы жестом переворота</string>
<string name="details_row_title_issuer">Эмитент</string>
<string name="details_row_title_signed_hashes">Подписано</string>
<string name="details_security_management_warning">Если вы забудете код, то потеряете доступ к своим средствам. Восстановление кода невозможно.</string>
<string name="details_title">Подробности</string>
<string name="disclaimer_error_loading">Проверьте подключение с интернетом или переключитесь на другую сеть</string>
<string name="disclaimer_title">Условия использования</string>
@ -189,7 +199,7 @@
<string name="express_choose_providers_title">Выберите провайдера</string>
<string name="express_error_code">Произошла ошибка. Код: %s</string>
<string name="express_error_provider_unavailable">К сожалению, обмен указанной пары через выбранного провайдера на данный момент невозможен. Попробуйте совершить обмен позже. (Код: %s)</string>
<string name="express_error_swap_pair_unavailable">Выбранный провайдер не доступен для обмена. Попробуйте позже. (Код: %s)</string>
<string name="express_error_swap_pair_unavailable">Выбранный провайдер недоступен для обмена. Попробуйте позже. (Код: %s)</string>
<string name="express_error_swap_unavailable">В данный момент обмен невозможен. Попробуйте позже. (Код: %s)</string>
<string name="express_estimated_amount">Курс обмена</string>
<string name="express_exchange_by">Обмен через %s</string>
@ -460,16 +470,17 @@
<string name="send_max_fee">Комиссия не превысит </string>
<string name="send_max_fee_footer">Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение.</string>
<string name="send_memo_destination_tag_error">Допустим ввод только цифр</string>
<string name="send_network_fee_warning_content">Сумма отправки будет уменьшена на %1$s для покрытия выбранного уровня комиссии. Получателю будет отправлено %2$s.</string>
<string name="send_network_fee_warning_content">Сумма отправки будет уменьшена на %1$s (%2$s) для покрытия выбранного уровня комиссии</string>
<string name="send_network_fee_warning_title">Покрытие сетевой комиссии</string>
<string name="send_notification_exceed_balance_text">Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса</string>
<string name="send_notification_exceed_balance_title">Недостаточно средств</string>
<string name="send_notification_existential_deposit_text">Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, убедитесь, что остаток после отправки будет не менее %s.</string>
<string name="send_notification_existential_deposit_button">Оставить %s</string>
<string name="send_notification_existential_deposit_text">Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, оставьте %s на балансе.</string>
<string name="send_notification_existential_deposit_title">Экзистенциальный депозит</string>
<string name="send_notification_fee_too_high_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
<string name="send_notification_fee_too_high_title">Установлена высокая комиссия</string>
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s.</string>
<string name="send_notification_high_fee_title">Комиссия увеличилась</string>
<string name="send_notification_high_fee_text">Ввиду особенности сети Tezos комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s.</string>
<string name="send_notification_high_fee_title">Комиссия повышена</string>
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению</string>
<string name="send_notification_invalid_amount_title">Недопустимая сумма</string>
<string name="send_notification_invalid_minimum_amount_text">Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %2$s.</string>
@ -490,7 +501,8 @@
<string name="send_recipient_label">Отправить</string>
<string name="send_recipient_memo_footer">Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств.</string>
<string name="send_recipient_wallets_title">Мои кошельки</string>
<string name="send_satoshi_per_byte_text">Это способ измерения комиссии за отправку биткоин-транзакции. Он указывает на количество самой маленькой единицы биткоина (сатоши) за каждый байт данных в транзакции. Чем выше это число, тем быстрее будет обработана транзакция сетью.</string>
<string name="send_satoshi_per_byte_text">Способ измерения комиссии за биткоин-транзакцию. Он указывает на количество самой маленькой единицы биткоина (сатоши) за каждый виртуальный байт в транзакции. Чем выше число, тем быстрее будет обработана транзакция майнерами.</string>
<string name="send_satoshi_per_byte_title">Сатоши / вбайт</string>
<string name="send_sending">Отправка</string>
<string name="send_summary_tap_hint">Нажмите на любое поле, чтобы изменить его</string>
<string name="send_summary_title">Отправка %s</string>
@ -547,6 +559,7 @@
<string name="token_button_unavailability_reason_not_exchangeable">В данный момент обмен монеты %s недоступен. Следите за нашими обновлениями.</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">Продажа средств станет доступной после завершения транзакции %s</string>
<string name="token_button_unavailability_reason_sell_unavailable">В данный момент продажа %s недоступна. Следите за нашими обновлениями.</string>
<string name="token_details_choose_address">Выберите адрес</string>
<string name="token_details_generate_xpub">Сгенерировать XPUB</string>
<string name="token_details_hide_alert_hide">Скрыть</string>
<string name="token_details_hide_alert_message">Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами.</string>

View file

@ -4,12 +4,16 @@
<string name="add_tokens_title">Manage tokens</string>
<string name="address_qr_code_message_format">Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.</string>
<string name="alert_button_request_support">Request support</string>
<string name="alert_button_try_again">Try again</string>
<string name="alert_demo_feature_disabled">This feature is disabled in Demo mode</string>
<string name="alert_failed_to_send_transaction_message">Reason: %s</string>
<string name="alert_failed_to_send_transaction_title">Can\'t send a transaction</string>
<string name="alert_manage_tokens_unsupported_blockchain_by_card_message">The selected does not support the %1$s network</string>
<string name="alert_manage_tokens_unsupported_curve_message">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.</string>
<string name="alert_manage_tokens_unsupported_message">Tokens in %1$s network are not supported by this card due to firmware limitation.</string>
<string name="alert_negative_app_rate_sent_message">Thank you for your feedback. We will respond as soon as possible</string>
<string name="alert_negative_app_rate_sent_title">Your suggestions were sent</string>
<string name="alert_troubleshooting_scan_card_message">Please try to tap the card exactly as shown in the animation or request support.</string>
<string name="alert_troubleshooting_scan_card_title">Are you having difficulty scanning your card?</string>
<string name="alert_unsupported_card">This card is not designed to work with this app</string>
<string name="app_settings_default_fee">Default Fee</string>
@ -25,6 +29,8 @@
<string name="app_settings_theme_mode_dark">Dark</string>
<string name="app_settings_theme_mode_light">Light</string>
<string name="app_settings_theme_mode_system">System default</string>
<string name="app_settings_theme_selection_footer">If system is selected, the app will auto-adjust based on your device\'s system settings</string>
<string name="app_settings_theme_selection_system_short">System</string>
<string name="app_settings_theme_selector_title">Theme</string>
<string name="app_settings_title">App Settings</string>
<string name="balance_hidden_description">To hide or show your balances, simply flip your device screen down, or switch it off in Settings</string>
@ -74,6 +80,7 @@
<string name="common_create">Create</string>
<string name="common_delete">Delete</string>
<string name="common_disabled">Disabled</string>
<string name="common_disconnect">Disconnect</string>
<string name="common_done">Done</string>
<string name="common_enable">Enable</string>
<string name="common_enabled">Enabled</string>
@ -106,6 +113,7 @@
<string name="common_reject">Reject</string>
<string name="common_reload">Reload</string>
<string name="common_rename">Rename</string>
<string name="common_retry">Retry</string>
<string name="common_save_changes">Save changes</string>
<string name="common_search">Search</string>
<string name="common_search_tokens">Search tokens</string>
@ -129,6 +137,7 @@
<string name="common_understand">I understand</string>
<string name="common_unknown_error">There was an error. Please try again.</string>
<string name="common_unreachable">Unreachable</string>
<string name="common_warning">Warning</string>
<string name="common_yes">Yes</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="currency_subtitle_expanded">Available networks</string>
@ -175,6 +184,7 @@
<string name="details_row_title_flip_to_hide">Flip-to-Hide Balances</string>
<string name="details_row_title_issuer">Issuer</string>
<string name="details_row_title_signed_hashes">Signed</string>
<string name="details_security_management_warning">If you forget the code you will lose access to your funds. Code recovery is not possible.</string>
<string name="details_title">Details</string>
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
<string name="disclaimer_title">Terms of Service</string>
@ -457,16 +467,17 @@
<string name="send_max_fee">Max fee</string>
<string name="send_max_fee_footer">The fee that will be charged for your transaction. You can set your own value.</string>
<string name="send_memo_destination_tag_error">Numbers only for Destination Tag</string>
<string name="send_network_fee_warning_content">Sending amount will be reduced by %1$s to cover the selected commission level. The recipient will get %2$s.</string>
<string name="send_network_fee_warning_content">Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level</string>
<string name="send_network_fee_warning_title">Network fee coverage</string>
<string name="send_notification_exceed_balance_text">Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance</string>
<string name="send_notification_exceed_balance_title">Total exceeds balance</string>
<string name="send_notification_existential_deposit_text">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.</string>
<string name="send_notification_existential_deposit_button">Leave %s</string>
<string name="send_notification_existential_deposit_text">The account will be wiped from the blockchain if a balance goes below the existential deposit. Please leave %s on your balance.</string>
<string name="send_notification_existential_deposit_title">Existential deposit</string>
<string name="send_notification_fee_too_high_text">The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.</string>
<string name="send_notification_fee_too_high_title">Custom fee is high</string>
<string name="send_notification_high_fee_text">The fee for transferring the entire balance is higher. To reduce the commission, you can leave %s.</string>
<string name="send_notification_high_fee_title">Fee is increased</string>
<string name="send_notification_high_fee_text">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.</string>
<string name="send_notification_high_fee_title">The fee is higher</string>
<string name="send_notification_invalid_amount_text">The included commission exceeds the transfer amount, leading to a negative value</string>
<string name="send_notification_invalid_amount_title">Invalid amount</string>
<string name="send_notification_invalid_minimum_amount_text">The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %2$s.</string>
@ -489,8 +500,8 @@
<string name="send_recipient_label">Send to</string>
<string name="send_recipient_memo_footer">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</string>
<string name="send_recipient_wallets_title">My wallets</string>
<string name="send_satoshi_per_byte_text">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.</string>
<string name="send_satoshi_per_byte_title">Satoshi per vbyte</string>
<string name="send_satoshi_per_byte_text">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.</string>
<string name="send_satoshi_per_byte_title">Satoshi / vByte</string>
<string name="send_sending">Sending...</string>
<string name="send_summary_tap_hint">Tap any field to change it</string>
<string name="send_summary_title">Send %s</string>
@ -548,6 +559,7 @@
<string name="token_button_unavailability_reason_not_exchangeable">Swapping %s is not available at the moment. Please check our updates.</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">Selling funds will be available once the %s transaction is complete</string>
<string name="token_button_unavailability_reason_sell_unavailable">Selling %s is not available at the moment. Please check our updates.</string>
<string name="token_details_choose_address">Choose address</string>
<string name="token_details_generate_xpub">Generate XPUB</string>
<string name="token_details_hide_alert_hide">Hide</string>
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.</string>

View file

@ -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(),
)
}
}

View file

@ -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

View file

@ -30,6 +30,7 @@ class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, Token
coin = currency,
isUnreachable = value.value.isError,
forceGrayscale = forceGrayscale,
showCustomBadge = showCustomTokenBadge,
)
is CryptoCurrency.Token -> getIconStateForToken(
token = currency,
@ -50,13 +51,14 @@ class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, Token
private fun getIconStateForCoin(
coin: CryptoCurrency.Coin,
isUnreachable: Boolean,
showCustomBadge: Boolean = true,
forceGrayscale: Boolean = false,
): TokenIconState.CoinIcon {
return TokenIconState.CoinIcon(
url = coin.iconUrl,
fallbackResId = coin.networkIconResId,
isGrayscale = forceGrayscale || coin.network.isTestnet || isUnreachable,
showCustomBadge = coin.isCustom,
showCustomBadge = coin.isCustom && showCustomBadge,
)
}
@ -76,6 +78,7 @@ class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, Token
background = background,
networkBadgeIconResId = token.networkIconResId,
isGrayscale = grayScale,
showCustomBadge = showCustomBadge,
)
} else {
TokenIconState.TokenIcon(

View file

@ -64,6 +64,8 @@ fun AmountTextField(
keyboardActions: KeyboardActions = KeyboardActions.Default,
isEnabled: Boolean = true,
isAutoResize: Boolean = false,
isValuePasted: Boolean = false,
onValuePastedTriggerDismiss: () -> 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,

View file

@ -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
}
}

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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<Quote>? = maybeQuotes?.fold(
ifLeft = {
quotesRetrievingFailed = true
null
},
ifRight = {
it.ifEmpty {
quotesRetrievingFailed = true
null
}
},
)
currencies.map { currency ->
val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }

View file

@ -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()
}

View file

@ -16,6 +16,20 @@ internal sealed class SendAnalyticEvents(
params: Map<String, String> = 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 {

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
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

View file

@ -20,6 +20,7 @@ import java.math.BigDecimal
* @param feeStateFactory [FeeStateFactory]
*/
internal class SendEventStateFactory(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
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 -> {

View file

@ -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),
),
)
}
}

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val userWalletProvider: Provider<UserWallet>,
private val appCurrencyProvider: Provider<AppCurrency>,
@ -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<SendNotification>): 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<SendNotification>,
): 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
}

View file

@ -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<SendEvent>,
)
) {
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<SendAmountSegmentedButtonsConfig>,
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<SendNotification>,
@ -105,6 +158,9 @@ enum class SendUiStateType {
None,
Recipient,
Amount,
Send,
Fee,
Send,
EditAmount,
EditRecipient,
EditFee,
}

View file

@ -18,6 +18,9 @@ internal class StateRouter(
val currentState: StateFlow<SendUiCurrentScreen>
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,

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) {
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)
}

View file

@ -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<String, BigDecimal?> {
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
}

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<Boolean, SendUiState> {
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 },
),
)
}

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
) : Converter<Boolean, SendUiState> {
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,
),
),
)
}
}

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<BigDecimal, SendUiState> {
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,
),
),
),
)
}
}

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<BigDecimal, SendUiState> {
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,
),
),
),
)
}
}

View file

@ -57,6 +57,7 @@ internal class SendAmountStateConverter(
),
),
isSegmentedButtonsEnabled = !noFeeRate,
selectedButton = 0,
)
}
}

View file

@ -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<SendUiState>,
) : Converter<Boolean, SendUiState> {
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,
)
}
}
}

View file

@ -10,12 +10,14 @@ internal class SendConfirmStateConverter(
) : Converter<Unit, SendStates.SendState> {
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(),

View file

@ -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<CryptoCurrencyStatus>,
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
@ -40,6 +42,7 @@ internal class SendNotificationFactory(
private val currencyChecksRepository: CurrencyChecksRepository,
private val stateRouterProvider: Provider<StateRouter>,
private val isSubtractAvailableProvider: Provider<Boolean>,
private val appCurrencyProvider: Provider<AppCurrency>,
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<SendNotification>.addFeeCoverageNotification(sendingAmount: Boolean) {
if (sendingAmount) {
private fun MutableList<SendNotification>.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<SendNotification>.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<SendNotification>.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"
}
}

View file

@ -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

View file

@ -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<StateRouter>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
) : Converter<FeeSelectorState.Content, Fee> {
@ -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,
)

View file

@ -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()

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val appCurrencyProvider: Provider<AppCurrency>,
@ -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<SendNotification>): 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),
),
)
}

View file

@ -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<StateRouter>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
) : Converter<Fee, ImmutableList<SendTextField.CustomFee>> {
@ -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,
)

View file

@ -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<StateRouter>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
) : CustomFeeConverter<Fee.Bitcoin> {
override fun convert(value: Fee.Bitcoin): ImmutableList<SendTextField.CustomFee> {
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(

View file

@ -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<StateRouter>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
) : CustomFeeConverter<Fee.Ethereum> {
override fun convert(value: Fee.Ethereum): ImmutableList<SendTextField.CustomFee> {
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<SendTextField.CustomFee>.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,
),
),

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<String, SendUiState> {
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
}
}

View file

@ -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<StateRouter>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val appCurrencyProvider: Provider<AppCurrency>,
) : Converter<String, SendTextField.AmountField> {
@ -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,

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<Unit, SendUiState> {
@ -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,

View file

@ -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()

View file

@ -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(

View file

@ -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<out SendNotification>) {}
override fun onAmountReduceClick(
reduceAmountBy: BigDecimal?,
reduceAmountTo: BigDecimal?,
clazz: Class<out SendNotification>,
) {}
override fun onNotificationCancel(clazz: Class<out SendNotification>) {}
}

View file

@ -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
}

View file

@ -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)

View file

@ -29,6 +29,7 @@ internal fun LazyListScope.buttons(
segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
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(

View file

@ -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)

View file

@ -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,
)
}
}

View file

@ -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,

View file

@ -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<Boolean, Boolean> {
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
}

View file

@ -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,

View file

@ -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,
)

View file

@ -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,

View file

@ -35,6 +35,7 @@ internal fun RecipientBlock(
Column(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(backgroundColor)
.clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick)

View file

@ -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)

View file

@ -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
}

View file

@ -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<out SendNotification>)
fun onAmountReduceClick(
reduceAmountBy: BigDecimal? = null,
reduceAmountTo: BigDecimal? = null,
clazz: Class<out SendNotification>,
)
fun onNotificationCancel(clazz: Class<out SendNotification>)
// endregion

View file

@ -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<GetFeeError, TransactionFee>? {
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<out SendNotification>) {
uiState = amountStateFactory.getOnAmountValueChange(reducedAmount.parseBigDecimal(cryptoCurrency.decimals))
override fun onAmountReduceClick(
reduceAmountBy: BigDecimal?,
reduceAmountTo: BigDecimal?,
clazz: Class<out SendNotification>,
) {
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

View file

@ -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) },
)
}

View file

@ -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

View file

@ -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,
)