Updated on 2026-08-14
This commit is contained in:
commit
b96f007568
69 changed files with 1367 additions and 564 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit fdf0660901442ade296db42bb867fc5c16a14262
|
||||
Subproject commit 07cfce37ff84e70081aca82d9948acb13c5c07b0
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
{
|
||||
"name": "LOCAL_USER_LOGS_ENABLED",
|
||||
"version": "5.8.0"
|
||||
"version": "5.10.0"
|
||||
},
|
||||
{
|
||||
"name": "GENERATE_XPUB_ENABLED",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,7 @@ internal class SendAmountStateConverter(
|
|||
),
|
||||
),
|
||||
isSegmentedButtonsEnabled = !noFeeRate,
|
||||
selectedButton = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>) {}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ internal fun RecipientBlock(
|
|||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(backgroundColor)
|
||||
.clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue