Updated on 2026-08-14

This commit is contained in:
Tangem 2024-04-24 13:49:45 +01:00
commit 856f6e1e53
52 changed files with 521 additions and 478 deletions

View file

@ -33,7 +33,9 @@ class ResetToFactorySettingsTask(
}
private fun resetBackup(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
if (session.environment.card?.backupStatus == Card.BackupStatus.NoBackup) {
if (session.environment.card?.backupStatus == null ||
session.environment.card?.backupStatus == Card.BackupStatus.NoBackup
) {
callback(CompletionResult.Success(session.environment.card!!))
return
}

View file

@ -13,11 +13,10 @@ import androidx.core.view.postDelayed
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
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
@ -54,17 +50,15 @@ import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentSendBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.text.DecimalFormatSymbols
import javax.inject.Inject
@ -73,6 +67,7 @@ private const val EDIT_TEXT_INPUT_DEBOUNCE = 400L
/**
[REDACTED_AUTHOR]
*/
@Suppress("LargeClass")
@OptIn(FlowPreview::class)
@AndroidEntryPoint
class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
@ -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
@ -103,18 +95,24 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
lifecycle.addObserver(viewModel)
sendSubscriber.initViewModel(viewModel)
Analytics.send(Token.Send.ScreenOpened())
listenToQrCode()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
subscribeToTransactionExtrasFields()
subscribeToAddressField()
subscribeToAmountField()
}
}
addBackPressHandler(this)
etAmountToSend = view.findViewById(R.id.etAmountToSend)
initSendButtonStates()
setupAddressLayout()
setupTransactionExtrasLayout()
setupAmountLayout()
setupFeeLayout()
setupWarningMessages()
@ -149,14 +147,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
setOnFocusChangeListener { _, hasFocus ->
store.dispatch(TruncateOrRestore(!hasFocus))
}
inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter { store.state.sendState.addressState.viewFieldValue.value != it }
.onEach {
store.dispatch(AddressActionUi.HandleUserInput(it))
}
.launchIn(mainScope)
}
imvPaste.setOnClickListener {
@ -183,31 +173,25 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
}
private fun listenToQrCode() {
lifecycleScope.launch {
listenToQrScanningUseCase(SourceType.SEND)
.getOrElse { emptyFlow() }
.flowWithLifecycle(this@SendFragment.lifecycle, minActiveState = Lifecycle.State.CREATED)
.collect { rawQr ->
delay(200)
// 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 etAmount error field is not displayed when
// inserting an incorrect amount by shareUri
cryptoCurrency?.let { cryptoCurrency ->
parseQrCodeUseCase(rawQr, cryptoCurrency = cryptoCurrency).fold(
ifLeft = {
onCodeScanned(QrResult(address = rawQr))
Timber.w(it)
},
ifRight = { onCodeScanned(it) },
)
} ?: onCodeScanned(QrResult(address = rawQr))
}
}
private fun CoroutineScope.subscribeToAddressField() = with(binding.lSendAddress) {
etAddress.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter { store.state.sendState.addressState.viewFieldValue.value != it }
.onEach {
store.dispatch(AddressActionUi.HandleUserInput(it))
}
.launchIn(this@subscribeToAddressField)
}
private fun setupTransactionExtrasLayout() = with(binding.lSendAddress) {
private fun CoroutineScope.subscribeToAmountField() {
etAmountToSend.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter { store.state.sendState.amountState.viewAmountValue.value != it && it.isNotEmpty() }
.onEach { store.dispatch(AmountActionUi.HandleUserInput(it)) }
.launchIn(this)
}
private fun CoroutineScope.subscribeToTransactionExtrasFields() = with(binding.lSendAddress) {
// TODO: [REDACTED_TASK_KEY]
etXlmMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
@ -216,7 +200,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
info.xlmMemo?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.XlmMemo.HandleUserInput(it)) }
.launchIn(mainScope)
.launchIn(this@subscribeToTransactionExtrasFields)
etDestinationTag.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
@ -225,7 +209,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
info.xrpDestinationTag?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.XrpDestinationTag.HandleUserInput(it)) }
.launchIn(mainScope)
.launchIn(this@subscribeToTransactionExtrasFields)
etBinanceMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
@ -234,7 +218,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
info.binanceMemo?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.BinanceMemo.HandleUserInput(it)) }
.launchIn(mainScope)
.launchIn(this@subscribeToTransactionExtrasFields)
etTonMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
@ -243,7 +227,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
info.tonMemoState?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.TonMemo.HandleUserInput(it)) }
.launchIn(mainScope)
.launchIn(this@subscribeToTransactionExtrasFields)
etCosmosMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
@ -252,7 +236,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
info.cosmosMemoState?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.CosmosMemo.HandleUserInput(it)) }
.launchIn(mainScope)
.launchIn(this@subscribeToTransactionExtrasFields)
etHederaMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
@ -261,7 +245,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
info.hederaMemoState?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.HederaMemo.HandleUserInput(it)) }
.launchIn(mainScope)
.launchIn(this@subscribeToTransactionExtrasFields)
etAlgorandMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
@ -270,22 +254,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
info.algorandMemoState?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.AlgorandMemo.HandleUserInput(it)) }
.launchIn(mainScope)
}
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))
.launchIn(this@subscribeToTransactionExtrasFields)
}
private fun setupAmountLayout() {
@ -343,12 +312,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
if (!hasFocus && etAmountToSend.text?.toString() == "") etAmountToSend.setText("0")
}
etAmountToSend.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter { store.state.sendState.amountState.viewAmountValue.value != it && it.isNotEmpty() }
.onEach { store.dispatch(AmountActionUi.HandleUserInput(it)) }
.launchIn(mainScope)
etAmountToSend.setOnImeActionListener(EditorInfo.IME_ACTION_DONE) {
it.hideSoftKeyboard()
it.clearFocus()

View file

@ -1,7 +1,11 @@
package com.tangem.tap.features.send.ui
import androidx.lifecycle.*
import arrow.core.getOrElse
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
@ -9,12 +13,16 @@ import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.di.DelayedWork
import com.tangem.tap.features.send.redux.AddressActionUi
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -31,10 +39,19 @@ internal class SendViewModel @Inject constructor(
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val parseQrCodeUseCase: ParseQrCodeUseCase,
@DelayedWork private val coroutineScope: CoroutineScope,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver {
init {
listenToQrScanningUseCase(SourceType.SEND)
.getOrElse { emptyFlow() }
.onEach(::onQRCodeScanned)
.launchIn(viewModelScope)
}
private val cryptoCurrency: CryptoCurrency? = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY]
override fun onCreate(owner: LifecycleOwner) {
@ -79,6 +96,43 @@ internal class SendViewModel @Inject constructor(
)
}
private fun onQRCodeScanned(qrScanResult: String) {
if (cryptoCurrency != null) {
parseQrCodeUseCase(qrScanResult, cryptoCurrency).fold(
ifRight = { parsedCode ->
store.dispatch(
AddressActionUi.PasteAddress(
data = parsedCode.address,
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
),
)
parsedCode.amount?.let { amount ->
store.dispatch(AmountAction.SetAmount(amount, isUserInput = false))
}
// parsedCode.memo?.let { }
},
ifLeft = {
store.dispatch(
AddressActionUi.PasteAddress(
data = qrScanResult,
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
),
)
Timber.w(it)
},
)
} else {
store.dispatch(
AddressActionUi.PasteAddress(
data = qrScanResult,
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
),
)
}
store.dispatch(AddressActionUi.TruncateOrRestore(truncate = true))
}
companion object {
private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L
private const val TAG = "SendViewModel"

View file

@ -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>
@ -430,8 +440,6 @@
<string name="scan_card_settings_message">Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку.</string>
<string name="scan_card_settings_title">Приготовьте свою карту</string>
<string name="send_additional_field_already_included">Уже содержится в введенном адресе</string>
<string name="send_alert_fee_coverage_subract_text">Вычесть</string>
<string name="send_alert_fee_coverage_title">Недостаточно средств для покрытия комиссии сети. Вычесть недостающую сумму для покрытия комиссии из отправляемой суммы?</string>
<string name="send_alert_fee_too_high_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
<string name="send_alert_fee_too_low_text">Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить?</string>
<string name="send_alert_transaction_failed_text">Причина: %1$s\nКод: %2$s</string>
@ -547,6 +555,7 @@
<string name="token_button_unavailability_reason_empty_balance">У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства.</string>
<string name="token_button_unavailability_reason_not_exchangeable">Обмен %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>
@ -656,6 +665,10 @@
<string name="warning_express_wrong_amount_description">Пожалуйста, измените сумму для обмена</string>
<string name="warning_failed_to_verify_card_message">Возможно, данная карта - образец или подделка</string>
<string name="warning_failed_to_verify_card_title">Ошибка проверки подлинности</string>
<string name="warning_hedera_missing_token_association_button_title">Ассоциировать</string>
<string name="warning_hedera_missing_token_association_message">Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять. Стоимость ассоциации ~%.4f %s</string>
<string name="warning_hedera_missing_token_association_message_brief">Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять</string>
<string name="warning_hedera_missing_token_association_title">Ассоциируете свой токен</string>
<string name="warning_low_signatures_message">На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства.</string>
<string name="warning_low_signatures_title">Малое количество подписей</string>
<string name="warning_manage_tokens_legacy_derivation_message">Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети.</string>

View file

@ -4,12 +4,16 @@
<string name="add_tokens_title">Manage tokens</string>
<string name="address_qr_code_message_format">Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.</string>
<string name="alert_button_request_support">Request support</string>
<string name="alert_button_try_again">Try again</string>
<string name="alert_demo_feature_disabled">This feature is disabled in Demo mode</string>
<string name="alert_failed_to_send_transaction_message">Reason: %s</string>
<string name="alert_failed_to_send_transaction_title">Can\'t send a transaction</string>
<string name="alert_manage_tokens_unsupported_blockchain_by_card_message">The selected does not support the %1$s network</string>
<string name="alert_manage_tokens_unsupported_curve_message">To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset.</string>
<string name="alert_manage_tokens_unsupported_message">Tokens in %1$s network are not supported by this card due to firmware limitation.</string>
<string name="alert_negative_app_rate_sent_message">Thank you for your feedback. We will respond as soon as possible</string>
<string name="alert_negative_app_rate_sent_title">Your suggestions were sent</string>
<string name="alert_troubleshooting_scan_card_message">Please try to tap the card exactly as shown in the animation or request support.</string>
<string name="alert_troubleshooting_scan_card_title">Are you having difficulty scanning your card?</string>
<string name="alert_unsupported_card">This card is not designed to work with this app</string>
<string name="app_settings_default_fee">Default Fee</string>
@ -25,6 +29,8 @@
<string name="app_settings_theme_mode_dark">Dark</string>
<string name="app_settings_theme_mode_light">Light</string>
<string name="app_settings_theme_mode_system">System default</string>
<string name="app_settings_theme_selection_footer">If system is selected, the app will auto-adjust based on your device\'s system settings</string>
<string name="app_settings_theme_selection_system_short">System</string>
<string name="app_settings_theme_selector_title">Theme</string>
<string name="app_settings_title">App Settings</string>
<string name="balance_hidden_description">To hide or show your balances, simply flip your device screen down, or switch it off in Settings</string>
@ -74,6 +80,7 @@
<string name="common_create">Create</string>
<string name="common_delete">Delete</string>
<string name="common_disabled">Disabled</string>
<string name="common_disconnect">Disconnect</string>
<string name="common_done">Done</string>
<string name="common_enable">Enable</string>
<string name="common_enabled">Enabled</string>
@ -106,6 +113,7 @@
<string name="common_reject">Reject</string>
<string name="common_reload">Reload</string>
<string name="common_rename">Rename</string>
<string name="common_retry">Retry</string>
<string name="common_save_changes">Save changes</string>
<string name="common_search">Search</string>
<string name="common_search_tokens">Search tokens</string>
@ -129,6 +137,7 @@
<string name="common_understand">I understand</string>
<string name="common_unknown_error">There was an error. Please try again.</string>
<string name="common_unreachable">Unreachable</string>
<string name="common_warning">Warning</string>
<string name="common_yes">Yes</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="currency_subtitle_expanded">Available networks</string>
@ -175,6 +184,7 @@
<string name="details_row_title_flip_to_hide">Flip-to-Hide Balances</string>
<string name="details_row_title_issuer">Issuer</string>
<string name="details_row_title_signed_hashes">Signed</string>
<string name="details_security_management_warning">If you forget the code you will lose access to your funds. Code recovery is not possible.</string>
<string name="details_title">Details</string>
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
<string name="disclaimer_title">Terms of Service</string>
@ -425,8 +435,6 @@
<string name="scan_card_settings_message">Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet.</string>
<string name="scan_card_settings_title">Get your card ready!</string>
<string name="send_additional_field_already_included">Already included in the entered address</string>
<string name="send_alert_fee_coverage_subract_text">Subtract</string>
<string name="send_alert_fee_coverage_title">Not enough funds to cover the network fee. Do you want to subtract the amount required to cover the fee?</string>
<string name="send_alert_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_alert_fee_too_low_text">You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue?</string>
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode: %2$s</string>
@ -548,6 +556,7 @@
<string name="token_button_unavailability_reason_empty_balance">You do not have funds to send. Top up your account to be able to send funds from it.</string>
<string name="token_button_unavailability_reason_not_exchangeable">%s swap is not available. But we are working on adding it.</string>
<string name="token_button_unavailability_reason_sell_unavailable">Sell of the %s coin is currently unavailable. But we are working on adding it.</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>
@ -657,6 +666,10 @@
<string name="warning_express_wrong_amount_description">Please change the amount to swap</string>
<string name="warning_failed_to_verify_card_message">This card might be a production sample or counterfeit</string>
<string name="warning_failed_to_verify_card_title">Authenticity check failed</string>
<string name="warning_hedera_missing_token_association_button_title">Associate</string>
<string name="warning_hedera_missing_token_association_message">This token must be associated with your Hedera account before you can receive it. Association fee ~%.4f %s</string>
<string name="warning_hedera_missing_token_association_message_brief">This token must be associated with your Hedera account before you can receive it</string>
<string name="warning_hedera_missing_token_association_title">Associate your token</string>
<string name="warning_low_signatures_message">Only %s signatures are left on this card. You must withdraw all of your funds.</string>
<string name="warning_low_signatures_title">Low signature count</string>
<string name="warning_manage_tokens_legacy_derivation_message">Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds.</string>

View file

@ -64,6 +64,8 @@ fun AmountTextField(
keyboardActions: KeyboardActions = KeyboardActions.Default,
isEnabled: Boolean = true,
isAutoResize: Boolean = false,
isValuePasted: Boolean = false,
onValuePastedTriggerDismiss: () -> Unit = {},
@FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
reduceFactor: Double = 0.9,
) {
@ -101,6 +103,8 @@ fun AmountTextField(
fontSize = fontSize,
textDirection = TextDirection.ContentOrLtr,
),
isValuePasted = isValuePasted,
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
color = textColor,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,

View file

@ -39,15 +39,13 @@ 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),
),
)
mutableStateOf(TextFieldValue(text = value))
}
val focusRequester = remember { FocusRequester.Default }
val customTextSelectionColors = TextSelectionColors(
@ -55,36 +53,28 @@ fun SimpleTextField(
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)
}
LaunchedEffect(key1 = isSelectionChanged) {
if (isSelectionChanged) {
textFieldValueState = textFieldValue
// resets paste value cursor trigger
LaunchedEffect(key1 = isValuePasted) {
if (isValuePasted) {
onValuePastedTriggerDismiss()
}
}
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),
)
}
mutableStateOf(value)
}
CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) {
BasicTextField(
value = textFieldValue,
value = textFieldValueState,
onValueChange = { newTextFieldValueState ->
textFieldValueState = newTextFieldValueState
@ -114,11 +104,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?,

View file

@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment.Companion.CenterEnd
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
@ -23,6 +23,7 @@ import com.tangem.core.ui.components.inputrow.inner.PasteButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import kotlinx.coroutines.delay
/**
* [Input Row Recipient](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-826&mode=design&t=IQ5lBJEkFGU4WSvi-4)
@ -115,8 +116,18 @@ fun InputRowRecipient(
@Composable
private fun RowScope.InputIcon(isLoading: Boolean, value: String) {
var isLoadingProxy by remember { mutableStateOf(isLoading) }
// Do not show the progress indicator, which will disappear quickly
LaunchedEffect(key1 = isLoading) {
if (isLoading) {
delay(timeMillis = 500)
}
isLoadingProxy = isLoading
}
AnimatedContent(
targetState = isLoading,
targetState = isLoadingProxy,
label = "Indicator Show Change",
modifier = Modifier
.align(CenterVertically)

View file

@ -6,9 +6,7 @@ import com.tangem.domain.qrscanning.models.QrResult
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.*
import java.math.BigDecimal
import java.net.URLDecoder

View file

@ -331,4 +331,8 @@ private val excludedBlockchains = listOf(
Blockchain.Nexa,
Blockchain.NexaTestnet,
Blockchain.Radiant,
Blockchain.Manta,
Blockchain.MantaTestnet,
Blockchain.Mantle,
Blockchain.MantleTestnet,
)

View file

@ -4,6 +4,7 @@ import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@ -15,6 +16,8 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.google.mlkit.vision.common.InputImage
import com.tangem.core.ui.res.TangemTheme
@ -25,6 +28,8 @@ import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
import com.tangem.feature.qrscanning.presentation.QrScanningContent
import com.tangem.feature.qrscanning.viewmodel.QrScanningViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import javax.inject.Inject
@ -69,11 +74,24 @@ internal class QrScanningFragment : ComposeFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.setRouter(innerRouter, galleryLauncher)
viewModel.setRouter(innerRouter)
cameraExecutor = Executors.newSingleThreadExecutor()
requestCameraPermission()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.launchGalleryEvent
.collect {
galleryLauncher.launch(it.imageFilter)
delay(timeMillis = 2000)
}
}
}
}
override fun onResume() {
super.onResume()
checkPermissionGranted()

View file

@ -1,6 +1,5 @@
package com.tangem.feature.qrscanning.viewmodel
import androidx.activity.result.ActivityResultLauncher
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
import kotlinx.coroutines.CoroutineScope
@ -11,23 +10,14 @@ internal open class BaseQrScanningClickIntents {
protected val router: QrScanningInnerRouter get() = _router
protected val viewModelScope: CoroutineScope get() = _viewModelScope
protected val source: SourceType get() = _source
protected val galleryLauncher: ActivityResultLauncher<String> get() = _galleryLauncher
private var _router: QrScanningInnerRouter by Delegates.notNull()
private var _viewModelScope: CoroutineScope by Delegates.notNull()
private var _source: SourceType by Delegates.notNull()
private var _galleryLauncher: ActivityResultLauncher<String> by Delegates.notNull()
open fun initialize(
router: QrScanningInnerRouter,
source: SourceType,
galleryLauncher: ActivityResultLauncher<String>,
coroutineScope: CoroutineScope,
) {
open fun initialize(router: QrScanningInnerRouter, source: SourceType, coroutineScope: CoroutineScope) {
_router = router
_viewModelScope = coroutineScope
_source = source
_galleryLauncher = galleryLauncher
}
}

View file

@ -5,11 +5,16 @@ import com.tangem.feature.qrscanning.presentation.QrScanningStateController
import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomSheetTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
interface QrScanningClickIntents {
val launchGallery: SharedFlow<GalleryRequest>
fun onBackClick()
fun onQrScanned(qrCode: String)
@ -17,6 +22,11 @@ interface QrScanningClickIntents {
fun onGalleryClicked()
}
@JvmInline
value class GalleryRequest(
val imageFilter: String,
)
@ViewModelScoped
internal class QrScanningClickIntentsImplementor @Inject constructor(
private val stateHolder: QrScanningStateController,
@ -26,6 +36,11 @@ internal class QrScanningClickIntentsImplementor @Inject constructor(
private var isScanned = false
override val launchGallery = MutableSharedFlow<GalleryRequest>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_LATEST,
)
override fun onBackClick() = router.popBackStack()
override fun onQrScanned(qrCode: String) {
@ -41,7 +56,7 @@ internal class QrScanningClickIntentsImplementor @Inject constructor(
}
override fun onGalleryClicked() {
galleryLauncher.launch(GALLERY_IMAGE_FILTER)
launchGallery.tryEmit(GalleryRequest(imageFilter = GALLERY_IMAGE_FILTER))
if (stateHolder.value.bottomSheetConfig != null) {
stateHolder.update(DismissBottomSheetTransformer())
}

View file

@ -1,6 +1,5 @@
package com.tangem.feature.qrscanning.viewmodel
import androidx.activity.result.ActivityResultLauncher
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -14,6 +13,7 @@ import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomShee
import com.tangem.feature.qrscanning.presentation.transformers.InitializeQrScanningStateTransformer
import com.tangem.feature.qrscanning.presentation.transformers.ShowCameraDeniedBottomSheetTransformer
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@ -28,12 +28,12 @@ internal class QrScanningViewModel @Inject constructor(
private val network: String? = savedStateHandle[NETWORK_KEY]
val uiState: StateFlow<QrScanningState> = stateHolder.uiState
val launchGalleryEvent: SharedFlow<GalleryRequest> = clickIntents.launchGallery
fun setRouter(router: QrScanningInnerRouter, galleryLauncher: ActivityResultLauncher<String>) {
fun setRouter(router: QrScanningInnerRouter) {
clickIntents.initialize(
router = router,
source = source,
galleryLauncher = galleryLauncher,
coroutineScope = viewModelScope,
)
stateHolder.update(InitializeQrScanningStateTransformer(clickIntents, source, network))

View file

@ -27,7 +27,6 @@ import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.referral.domain.models.ExpectedAward
import com.tangem.feature.referral.domain.models.ExpectedAwards
@ -350,7 +349,7 @@ private fun BoxScope.ErrorSnackbarHost(errorSnackbar: ErrorSnackbar?) {
modifier = Modifier.fillMaxWidth(),
actionOnNewLine = true,
shape = RoundedCornerShape(size = TangemTheme.dimens.radius8),
containerColor = TangemColorPalette.Black,
containerColor = TangemTheme.colors.button.primary,
contentColor = TangemTheme.colors.text.primary2,
actionColor = TangemTheme.colors.text.primary2,
)
@ -464,7 +463,7 @@ private fun Preview_ReferralScreen_Participant_InLightTheme() {
url = "",
expectedAwards = null,
),
errorSnackbar = null,
errorSnackbar = ErrorSnackbar(DemoModeException()) {},
analytics = Analytics(
onAgreementClicked = {},
onCopyClicked = {},
@ -493,7 +492,7 @@ private fun Preview_ReferralScreen_Participant_InDarkTheme() {
url = "",
expectedAwards = null,
),
errorSnackbar = null,
errorSnackbar = ErrorSnackbar(DemoModeException()) {},
analytics = Analytics(
onAgreementClicked = {},
onCopyClicked = {},

View file

@ -4,27 +4,18 @@ import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.ui.SendScreen
import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch
import java.lang.ref.WeakReference
import javax.inject.Inject
@ -40,9 +31,6 @@ internal class SendFragment : ComposeFragment() {
@Inject
lateinit var router: SendRouter
@Inject
lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase
@Inject
lateinit var analyticsEventsHandler: AnalyticsEventHandler
@ -65,7 +53,6 @@ internal class SendFragment : ComposeFragment() {
analyticsEventsHandler = analyticsEventsHandler,
),
)
listenToQrCode()
}
@Composable
@ -83,25 +70,7 @@ internal class SendFragment : ComposeFragment() {
super.onDestroy()
}
private fun listenToQrCode() {
lifecycleScope.launch {
listenToQrScanningUseCase(SourceType.SEND)
.getOrElse { emptyFlow() }
.flowWithLifecycle(this@SendFragment.lifecycle, minActiveState = Lifecycle.State.CREATED)
.collect {
delay(QR_SCAN_DELAY)
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
// If do not use the delay, then error field is not displayed when
// inserting an incorrect amount by shareUri
viewModel.onQrCodeScanned(it)
}
}
}
companion object {
private const val QR_SCAN_DELAY = 200L
/** Create send fragment instance */
fun create(): SendFragment = SendFragment()
}

View file

@ -103,6 +103,10 @@ internal sealed class SendAnalyticEvents(
params = mapOf(TOKEN to token),
)
data object NoticeFeeCoverage : SendAnalyticEvents(
event = "Notice - Network Fee Coverage",
)
/** If error occurs during send transactions */
data class TransactionError(val token: String) : SendAnalyticEvents(
event = "Error - Transaction Rejected",

View file

@ -68,16 +68,6 @@ internal sealed class SendAlertState {
override val confirmButtonText: TextReference = resourceReference(R.string.common_continue)
}
data class FeeCoverage(
override val onConfirmClick: (() -> Unit),
) : SendAlertState() {
override val title: TextReference? = null
override val message: TextReference =
resourceReference(id = R.string.send_alert_fee_coverage_title)
override val confirmButtonText: TextReference =
resourceReference(id = R.string.send_alert_fee_coverage_subract_text)
}
data class ReserveAmount(val amount: String) : SendAlertState() {
override val title: TextReference =
resourceReference(id = R.string.send_notification_invalid_reserve_amount_title, wrappedList(amount))

View file

@ -48,19 +48,6 @@ internal class SendEventStateFactory(
)
}
fun getFeeCoverageAlert(onConsume: () -> Unit): SendUiState {
return currentStateProvider().copy(
event = triggeredEvent(
data = SendEvent.ShowAlert(
SendAlertState.FeeCoverage(
onConfirmClick = clickIntents::onSubtractSelect,
),
),
onConsume = onConsume,
),
)
}
fun getFeeUpdatedAlert(fee: TransactionFee, onConsume: () -> Unit, onFeeNotIncreased: () -> Unit): SendUiState {
val state = currentStateProvider()
val feeSelector = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state
@ -139,9 +126,7 @@ internal class SendEventStateFactory(
return state.copy(
event = triggeredEvent(
data = SendEvent.ShowAlert(
SendAlertState.FeeUnreachableError(
onConfirmClick = { clickIntents.feeReload(true) },
),
SendAlertState.FeeUnreachableError(onConfirmClick = clickIntents::feeReload),
),
onConsume = onConsume,
),

View file

@ -42,11 +42,6 @@ internal sealed class SendNotification(val config: NotificationConfig) {
),
)
data class ReserveAmountError(val amount: String) : Error(
title = resourceReference(R.string.send_notification_invalid_reserve_amount_title, wrappedList(amount)),
subtitle = resourceReference(R.string.send_notification_invalid_reserve_amount_text),
)
data class TransactionLimitError(
val cryptoCurrency: String,
val utxoLimit: String,
@ -153,5 +148,10 @@ internal sealed class SendNotification(val config: NotificationConfig) {
onClick = onRefresh,
),
)
data object FeeCoverageNotification : Warning(
title = resourceReference(R.string.send_network_fee_warning_title),
subtitle = resourceReference(R.string.swapping_network_fee_warning_content),
)
}
}

View file

@ -14,8 +14,8 @@ 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.amount.SendAmountSubtractConverter
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.fields.SendAmountFieldConverter
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientHistoryListConverter
@ -48,12 +48,6 @@ internal class SendStateFactory(
appCurrencyProvider = appCurrencyProvider,
)
}
private val amountSubtractConverter by lazy(LazyThreadSafetyMode.NONE) {
SendAmountSubtractConverter(
currentStateProvider = currentStateProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendAmountStateConverter(
appCurrencyProvider = appCurrencyProvider,
@ -265,20 +259,12 @@ internal class SendStateFactory(
//endregion
//region send
fun onSubtractSelect(isAmountSubtractAvailable: Boolean): SendUiState {
val state = currentStateProvider()
if (!isAmountSubtractAvailable) return state
return amountSubtractConverter.convert(Unit)
}
fun getSendingStateUpdate(isSending: Boolean): SendUiState {
val state = currentStateProvider()
return state.copy(
sendState = state.sendState?.copy(
isSending = isSending,
isPrimaryButtonEnabled = !isSending,
isPrimaryButtonEnabled = isPrimaryButtonEnabled(state, state.sendState.notifications),
),
)
}
@ -300,10 +286,9 @@ 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, notifications),
notifications = notifications,
showTapHelp = sendState.showTapHelp && notifications.isEmpty(),
),
@ -317,5 +302,12 @@ internal class SendStateFactory(
sendState = sendState.copy(showTapHelp = false),
)
}
private fun isPrimaryButtonEnabled(state: SendUiState, notifications: ImmutableList<SendNotification>): Boolean {
val sendState = state.sendState ?: return false
val feeState = state.feeState ?: return false
val hasErrorNotifications = notifications.any { it is SendNotification.Error }
return !hasErrorNotifications && !sendState.isSending && feeState.feeSelectorState is FeeSelectorState.Content
}
//endregion
}

View file

@ -50,10 +50,7 @@ internal sealed class SendStates {
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
val isSegmentedButtonsEnabled: Boolean,
val amountTextField: SendTextField.AmountField,
val notifications: ImmutableList<SendNotification>,
val appCurrencyCode: String,
val isFeeLoading: Boolean,
val subtractedFee: BigDecimal?,
) : SendStates()
/** Recipient state */
@ -87,10 +84,9 @@ 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 isSubtract: Boolean,
val transactionDate: Long,
val txUrl: String,
val ignoreAmountReduce: Boolean,

View file

@ -1,12 +1,10 @@
package com.tangem.features.send.impl.presentation.state.amount
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
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.fields.SendAmountFieldChangeConverter
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldMaxAmountConverter
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
/**
* Factory to produce amount state for [SendUiState]
@ -35,6 +33,11 @@ internal class AmountStateFactory(
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
private val amountPasteConverter by lazy(LazyThreadSafetyMode.NONE) {
SendAmountPastedTriggerDismissConverter(
currentStateProvider = currentStateProvider,
)
}
fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value)
@ -44,17 +47,5 @@ internal class AmountStateFactory(
fun getOnCurrencyChangedState(isFiat: Boolean) = amountCurrencyConverter.convert(isFiat)
fun getAmountNotificationState(notifications: ImmutableList<SendNotification>): SendUiState {
val state = currentStateProvider()
return state.copy(
amountState = state.amountState?.copy(notifications = notifications),
)
}
fun getOnAmountFeeLoadingCancel(): SendUiState {
val state = currentStateProvider()
return state.copy(
amountState = state.amountState?.copy(isFeeLoading = false),
)
}
fun getOnAmountPastedTriggerDismiss() = amountPasteConverter.convert(false)
}

View file

@ -24,6 +24,7 @@ internal class SendAmountCurrencyConverter(
amountState = amountState.copy(
amountTextField = amountTextField.copy(
isFiatValue = value,
isValuePasted = true,
),
),
)

View file

@ -0,0 +1,21 @@
package com.tangem.features.send.impl.presentation.state.amount
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
internal class SendAmountPastedTriggerDismissConverter(
private val currentStateProvider: Provider<SendUiState>,
) : Converter<Boolean, SendUiState> {
override fun convert(value: Boolean): SendUiState {
val state = currentStateProvider()
val amountState = state.amountState ?: return state
return state.copy(
amountState = amountState.copy(
amountTextField = amountState.amountTextField.copy(
isValuePasted = false,
),
),
)
}
}

View file

@ -39,10 +39,7 @@ internal class SendAmountStateConverter(
tokenIconState = iconStateConverter.convert(status),
amountTextField = sendAmountFieldConverter.convert(value),
isPrimaryButtonEnabled = false,
notifications = persistentListOf(),
isFeeLoading = false,
appCurrencyCode = appCurrency.code,
subtractedFee = null,
segmentedButtonConfig = persistentListOf(
SendAmountSegmentedButtonsConfig(
title = stringReference(status.currency.symbol),

View file

@ -1,50 +0,0 @@
package com.tangem.features.send.impl.presentation.state.amount
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.utils.Provider
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
internal class SendAmountSubtractConverter(
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<Unit, SendUiState> {
override fun convert(value: Unit): SendUiState {
val state = currentStateProvider()
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val amountState = state.amountState ?: return state
val feeState = state.feeState ?: return state
val feeValue = feeState.fee?.amount?.value ?: return state
val amountTextField = amountState.amountTextField
val amountValue = amountTextField.cryptoAmount.value ?: return state
val balance = cryptoCurrencyStatus.value.amount ?: return state
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val cryptoDecimals = amountTextField.cryptoAmount.decimals
val fiatDecimals = amountTextField.fiatAmount.decimals
val feeDiff = amountState.subtractedFee?.let { feeValue.minus(it) }
?: amountValue.minus(balance.minus(feeValue))
val decimalCryptoValue = amountValue.minus(feeDiff)
if (decimalCryptoValue < BigDecimal.ZERO) return state
val decimalFiatValue = decimalCryptoValue.multiply(fiatRate)
val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals)
val fiatValue = decimalFiatValue.parseBigDecimal(fiatDecimals)
return state.copy(
sendState = state.sendState?.copy(isSubtract = true),
amountState = amountState.copy(
subtractedFee = feeValue,
amountTextField = amountTextField.copy(
value = cryptoValue,
fiatValue = fiatValue,
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
),
),
)
}
}

View file

@ -10,10 +10,9 @@ 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,
isSubtract = false,
transactionDate = 0L,
txUrl = "",
ignoreAmountReduce = false,

View file

@ -18,11 +18,10 @@ import com.tangem.domain.wallets.models.UserWallet
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.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.state.fee.checkIfFeeTooHigh
import com.tangem.features.send.impl.presentation.state.fee.*
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
import kotlinx.collections.immutable.persistentListOf
@ -36,11 +35,11 @@ import java.math.BigDecimal
internal class SendNotificationFactory(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feePaidCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val currentStateProvider: Provider<SendUiState>,
private val userWalletProvider: Provider<UserWallet>,
private val currencyChecksRepository: CurrencyChecksRepository,
private val stateRouterProvider: Provider<StateRouter>,
private val isSubtractAvailableProvider: Provider<Boolean>,
private val clickIntents: SendClickIntents,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
@ -52,19 +51,33 @@ internal class SendNotificationFactory(
val state = currentStateProvider()
val sendState = state.sendState ?: return@map persistentListOf()
val feeState = state.feeState ?: return@map persistentListOf()
val feeAmount = feeState.fee?.amount?.value ?: BigDecimal.ZERO
val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO
val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO
val isFeeCoverage = checkFeeCoverage(
isSubtractAvailable = isSubtractAvailableProvider(),
balance = balance,
amountValue = amountValue,
feeValue = feeValue,
)
val sendingAmount = calculateSubtractedAmount(
isFeeCoverage = isFeeCoverage,
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
amountValue = amountValue,
feeValue = feeValue,
)
buildList {
// errors
addFeeUnreachableNotification(feeState.feeSelectorState)
addExceedBalanceNotification(feeAmount, amountValue)
addExceedBalanceNotification(feeValue, sendingAmount)
addExceedsBalanceNotification(feeState.fee)
addMinimumAmountErrorNotification(feeAmount, amountValue)
addDustWarningNotification(feeAmount, amountValue)
addTransactionLimitErrorNotification(feeAmount, amountValue)
addMinimumAmountErrorNotification(feeValue, sendingAmount)
addDustWarningNotification(feeValue, sendingAmount)
addTransactionLimitErrorNotification(feeValue, sendingAmount)
// warnings
addExistentialWarningNotification(feeAmount, amountValue)
addHighFeeWarningNotification(feeAmount, amountValue, sendState.ignoreAmountReduce)
addExistentialWarningNotification(feeValue, amountValue)
addFeeCoverageNotification(isFeeCoverage)
addHighFeeWarningNotification(amountValue, sendState.ignoreAmountReduce)
addTooHighNotification(feeState.feeSelectorState)
addTooLowNotification(feeState)
}.toImmutableList()
@ -95,13 +108,11 @@ internal class SendNotificationFactory(
receivedAmount: BigDecimal,
) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val feePaidCryptoCurrencyStatus = feePaidCryptoCurrencyStatusProvider()
val cryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val isCurrentNotFeePaidCurrency = cryptoCurrencyStatus.currency.id != feePaidCryptoCurrencyStatus?.currency?.id
if (isCurrentNotFeePaidCurrency) return
if (!isSubtractAvailableProvider()) return
val showNotification = receivedAmount + feeAmount > cryptoAmount
val showNotification = receivedAmount + feeAmount > balance
if (showNotification) {
add(SendNotification.Error.TotalExceedsBalance)
}
@ -192,16 +203,22 @@ internal class SendNotificationFactory(
}
}
private fun MutableList<SendNotification>.addFeeCoverageNotification(sendingAmount: Boolean) {
if (sendingAmount) {
analyticsEventHandler.send(SendAnalyticEvents.NoticeFeeCoverage)
add(SendNotification.Warning.FeeCoverageNotification)
}
}
private fun MutableList<SendNotification>.addHighFeeWarningNotification(
feeAmount: BigDecimal,
sendAmount: BigDecimal,
ignoreAmountReduce: Boolean,
) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val isTezos = cryptoCurrencyStatus.currency.network.id.value == Blockchain.Tezos.id
val isTezos = isTezos(cryptoCurrencyStatus.currency.network.id.value)
val threshold = Blockchain.Tezos.minimalAmount()
val isTotalBalance = feeAmount.plus(sendAmount) >= balance && balance > threshold
val isTotalBalance = sendAmount >= balance && balance > threshold
if (!ignoreAmountReduce && isTotalBalance && isTezos) {
add(
SendNotification.Warning.HighFeeError(
@ -218,6 +235,7 @@ internal class SendNotificationFactory(
}
}
// todo remove in [REDACTED_TASK_KEY]
private fun MutableList<SendNotification>.addMinimumAmountErrorNotification(
feeAmount: BigDecimal,
receivedAmount: BigDecimal,

View file

@ -1,21 +1,73 @@
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.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.common.extensions.minimalAmount
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
/**
* Check if sending amount with fee is greater than balance
* Check and calculates subtracted amount
*/
internal fun checkFeeCoverage(state: SendUiState, cryptoCurrencyStatus: CryptoCurrencyStatus): Boolean {
val balance = cryptoCurrencyStatus.value.amount ?: return false
val fee = state.feeState?.fee?.amount?.value ?: return false
val amount = state.amountState?.amountTextField?.cryptoAmount?.value ?: return false
return balance < amount + fee && balance > fee
internal fun checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountValue: BigDecimal,
feeValue: BigDecimal,
): BigDecimal {
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
val isFeeCoverage = checkFeeCoverage(
isSubtractAvailable = isAmountSubtractAvailable,
balance = balance,
amountValue = amountValue,
feeValue = feeValue,
)
return calculateSubtractedAmount(
isFeeCoverage = isFeeCoverage,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
)
}
/**
* Checks if sending amount with fee is greater than balance
*/
internal fun checkFeeCoverage(
isSubtractAvailable: Boolean,
balance: BigDecimal,
amountValue: BigDecimal,
feeValue: BigDecimal,
): Boolean {
if (!isSubtractAvailable) return false
return balance < amountValue + feeValue && balance > feeValue
}
/**
* Calculates subtracted amount
*/
internal fun calculateSubtractedAmount(
isFeeCoverage: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountValue: BigDecimal,
feeValue: BigDecimal,
): 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
} else {
amountValue
}
}
/**

View file

@ -45,7 +45,9 @@ internal class FeeStateFactory(
val state = currentStateProvider()
val feeState = state.feeState ?: return state
return state.copy(
amountState = state.amountState?.copy(isFeeLoading = true),
sendState = state.sendState?.copy(
isPrimaryButtonEnabled = false,
),
feeState = feeState.copy(
feeSelectorState = if (feeState.feeSelectorState is FeeSelectorState.Content) {
feeState.feeSelectorState
@ -78,7 +80,9 @@ internal class FeeStateFactory(
val fee = feeConverter.convert(updatedFeeSelectorState)
return state.copy(
amountState = state.amountState?.copy(isFeeLoading = false),
sendState = state.sendState?.copy(
isPrimaryButtonEnabled = true,
),
feeState = feeState.copy(
feeSelectorState = updatedFeeSelectorState,
fee = fee,
@ -90,10 +94,12 @@ internal class FeeStateFactory(
fun onFeeOnErrorState(): SendUiState {
val state = currentStateProvider()
return state.copy(
amountState = state.amountState?.copy(isFeeLoading = false),
feeState = state.feeState?.copy(
feeSelectorState = FeeSelectorState.Error,
),
sendState = state.sendState?.copy(
isPrimaryButtonEnabled = false,
),
)
}

View file

@ -38,7 +38,6 @@ internal class SendAmountFieldChangeConverter(
return state.copy(
amountState = amountState.copy(
isPrimaryButtonEnabled = !isExceedBalance && !isZero,
subtractedFee = null,
amountTextField = amountTextField.copy(
value = cryptoValue,
fiatValue = fiatValue,

View file

@ -53,8 +53,10 @@ internal class SendAmountFieldConverter(
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,
)
}

View file

@ -34,8 +34,8 @@ internal class SendAmountFieldMaxAmountConverter(
return state.copy(
amountState = amountState.copy(
isPrimaryButtonEnabled = true,
subtractedFee = null,
amountTextField = amountTextField.copy(
isValuePasted = true,
value = cryptoValue,
fiatValue = fiatValue,
isError = false,

View file

@ -28,6 +28,8 @@ internal sealed class SendTextField {
val isFiatValue: Boolean,
val fiatValue: String,
val isFiatUnavailable: Boolean,
val isValuePasted: Boolean,
val onValuePastedTriggerDismiss: () -> Unit,
val isError: Boolean,
val error: TextReference,
) : SendTextField()

View file

@ -22,10 +22,7 @@ internal object AmountStatePreviewData {
walletBalance = stringReference("123.123"),
tokenIconState = TokenIconState.Loading,
segmentedButtonConfig = persistentListOf(),
notifications = persistentListOf(),
isFeeLoading = false,
appCurrencyCode = "usd",
subtractedFee = null,
amountTextField = SendTextField.AmountField(
value = "123.123123123123123123",
onValueChange = {},
@ -48,6 +45,8 @@ internal object AmountStatePreviewData {
isFiatUnavailable = false,
isError = false,
error = TextReference.EMPTY,
isValuePasted = false,
onValuePastedTriggerDismiss = {},
),
isSegmentedButtonsEnabled = true,
)

View file

@ -30,18 +30,18 @@ internal object SendClickIntentsStub : SendClickIntents {
override fun onMaxValueClick() {}
override fun onAmountPasteTriggerDismiss() {}
override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {}
override fun onRecipientMemoValueChange(value: String) {}
override fun feeReload(isToNextState: Boolean) {}
override fun feeReload() {}
override fun onFeeSelectorClick(feeType: FeeType) {}
override fun onCustomFeeValueChange(index: Int, value: String) {}
override fun onSubtractSelect() {}
override fun onReadMoreClick() {}
override fun onSendClick() {}

View file

@ -7,7 +7,7 @@ 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
@ -29,6 +29,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType
import kotlinx.coroutines.delay
@Composable
internal fun SendNavigationButtons(
@ -40,6 +41,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(
@ -78,7 +80,6 @@ private fun SendNavigationButton(
val isFromConfirmation = currentState.isFromConfirmation
val isCorrectScreen = currentState.type == SendUiStateType.Amount || currentState.type == SendUiStateType.Fee
val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess && !isSending
val showProgress = uiState.amountState?.isFeeLoading == true
val (buttonTextId, buttonClick) = getButtonData(
currentState = currentState,
@ -92,25 +93,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),
@ -120,7 +122,7 @@ private fun SendNavigationButton(
if (isSendingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
buttonClick()
},
showProgress = showProgress,
showProgress = false,
modifier = Modifier.fillMaxWidth(),
colors = TangemButtonsDefaults.primaryButtonColors,
)
@ -129,8 +131,18 @@ private fun SendNavigationButton(
@Composable
private fun SendingText(uiState: SendUiState, isVisible: Boolean, modifier: Modifier = Modifier) {
var isVisibleProxy by remember { mutableStateOf(isVisible) }
// text appearance delay for smooth screen transitions
LaunchedEffect(key1 = isVisible) {
if (isVisible) {
delay(timeMillis = 400)
}
isVisibleProxy = isVisible
}
AnimatedVisibility(
visible = isVisible,
visible = isVisibleProxy,
modifier = modifier,
enter = slideInVertically().plus(fadeIn()),
exit = slideOutVertically().plus(fadeOut()),

View file

@ -1,10 +1,8 @@
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.*
import androidx.compose.animation.core.tween
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.SnackbarHostState
@ -24,6 +22,8 @@ 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.*
@Composable
internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) {
@ -88,27 +88,46 @@ internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen)
)
}
@OptIn(ExperimentalAnimationApi::class)
@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
} else {
AnimatedContentTransitionScope.SlideDirection.End
}
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,
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(

View file

@ -28,7 +28,6 @@ private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey"
internal fun LazyListScope.buttons(
segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
clickIntents: SendClickIntents,
isMaxButtonEnabled: Boolean,
isSegmentedButtonsEnabled: Boolean,
) {
item(
@ -61,7 +60,6 @@ internal fun LazyListScope.buttons(
}
SecondaryButton(
text = stringResource(R.string.send_max_amount),
enabled = isMaxButtonEnabled,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clickIntents.onMaxValueClick()

View file

@ -22,11 +22,10 @@ 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.delay
import kotlinx.coroutines.job
@Composable
internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolean, appCurrencyCode: String) {
internal fun AmountField(sendField: SendTextField.AmountField, appCurrencyCode: String) {
val decimalFormat = rememberDecimalFormat()
val isFiatValue = sendField.isFiatValue
val currencyCode = if (isFiatValue) appCurrencyCode else null
@ -36,15 +35,6 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea
sendField.cryptoAmount to sendField.value
}
val requester = remember { FocusRequester() }
var isEnabledProxy by remember { mutableStateOf(isEnabled) }
// Fix animation from amount screen to summary screen ([REDACTED_TASK_KEY])
LaunchedEffect(key1 = isEnabled) {
if (isEnabled) {
delay(timeMillis = 700)
}
isEnabledProxy = isEnabled
}
AmountTextField(
value = primaryValue,
@ -62,8 +52,9 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
),
isEnabled = isEnabledProxy,
isAutoResize = true,
isValuePasted = sendField.isValuePasted,
onValuePastedTriggerDismiss = sendField.onValuePastedTriggerDismiss,
modifier = Modifier
.focusRequester(requester)
.padding(

View file

@ -62,7 +62,6 @@ internal fun LazyListScope.amountField(
)
AmountField(
sendField = amountState.amountTextField,
isEnabled = !amountState.isFeeLoading,
appCurrencyCode = amountState.appCurrencyCode,
)
}

View file

@ -12,7 +12,6 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData
import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub
import com.tangem.features.send.impl.presentation.ui.common.notifications
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
@Composable
@ -22,6 +21,7 @@ internal fun SendAmountContent(
clickIntents: SendClickIntents,
) {
if (amountState == null) return
// Do not put fillMaxSize() in here
LazyColumn(
modifier = Modifier
.padding(
@ -35,10 +35,8 @@ internal fun SendAmountContent(
buttons(
segmentedButtonConfig = amountState.segmentedButtonConfig,
clickIntents = clickIntents,
isMaxButtonEnabled = !amountState.isFeeLoading,
isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled,
)
notifications(amountState.notifications)
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.features.send.impl.presentation.ui.fee
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@ -27,8 +26,7 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S
val isCustomSelected = feeSendState?.selectedFee == FeeType.Custom
val hasNotifications = notifications.isNotEmpty()
LazyColumn(
modifier = Modifier
.fillMaxSize()
modifier = Modifier // Do not put fillMaxSize() in here
.background(TangemTheme.colors.background.tertiary)
.padding(
start = TangemTheme.dimens.spacing16,

View file

@ -4,7 +4,6 @@ import androidx.annotation.StringRes
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@ -48,8 +47,7 @@ internal fun SendRecipientContent(
val isValidating by remember(uiState.isValidating) { derivedStateOf { uiState.isValidating } }
val isError by remember(address.isError) { derivedStateOf { address.isError } }
LazyColumn(
modifier = Modifier
.fillMaxSize()
modifier = Modifier // Do not put fillMaxSize() in here
.background(TangemTheme.colors.background.tertiary)
.padding(
start = TangemTheme.dimens.spacing16,

View file

@ -4,7 +4,10 @@ import androidx.compose.animation.*
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Icon
@ -33,9 +36,7 @@ private const val TAP_HELP_ANIMATION_DELAY = 500L
internal fun SendContent(uiState: SendUiState) {
val sendState = uiState.sendState ?: return
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = TangemTheme.dimens.spacing16),
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
blocks(uiState)
tapHelp(isDisplay = sendState.showTapHelp)

View file

@ -30,6 +30,8 @@ internal interface SendClickIntents {
fun onCurrencyChangeClick(isFiat: Boolean)
fun onMaxValueClick()
fun onAmountPasteTriggerDismiss()
// endregion
// region Recipient
@ -39,14 +41,12 @@ internal interface SendClickIntents {
// endregion
// region Fee
fun feeReload(isToNextState: Boolean = false)
fun feeReload()
fun onFeeSelectorClick(feeType: FeeType)
fun onCustomFeeValueChange(index: Int, value: String)
fun onSubtractSelect()
fun onReadMoreClick()
// endregion

View file

@ -15,6 +15,8 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
@ -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,
@ -157,10 +160,10 @@ internal class SendViewModel @Inject constructor(
private val sendNotificationFactory = SendNotificationFactory(
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
feePaidCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
currentStateProvider = Provider { uiState },
userWalletProvider = Provider { userWallet },
stateRouterProvider = Provider { stateRouter },
isSubtractAvailableProvider = Provider { isAmountSubtractAvailable },
currencyChecksRepository = currencyChecksRepository,
clickIntents = this,
analyticsEventHandler = analyticsEventHandler,
@ -189,12 +192,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
private var feeIdleTimer = 0L
init {
subscribeOnQRScannerResult()
subscribeOnCurrencyStatusUpdates()
subscribeOnBalanceHidden()
getTapHelpPreviewAvailability()
@ -216,6 +218,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(
@ -488,12 +497,7 @@ internal class SendViewModel @Inject constructor(
if (onFeeNext()) return
}
SendUiStateType.Amount -> {
if (uiState.feeState?.feeSelectorState is FeeSelectorState.Content) {
if (onFeeCoverageAlert()) return
} else {
loadFee(isToNextState = true)
return
}
loadFee()
}
else -> Unit
}
@ -519,7 +523,6 @@ internal class SendViewModel @Inject constructor(
innerRouter.openTokenDetails(userWalletId, currency)
private fun onFeeNext(): Boolean {
if (onFeeCoverageAlert()) return true
if (checkIfFeeTooLow(uiState)) {
uiState = eventStateFactory.getFeeTooLowAlert(
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
@ -538,24 +541,28 @@ internal class SendViewModel @Inject constructor(
)
}
private fun onFeeCoverageAlert(): Boolean {
val isFeeCoverage = checkFeeCoverage(uiState, cryptoCurrencyStatus)
return if (isAmountSubtractAvailable && isFeeCoverage) {
uiState = eventStateFactory.getFeeCoverageAlert(
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
)
true
} else {
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
false
}
}
private fun cancelFeeRequest() {
viewModelScope.launch(dispatchers.main) {
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
@ -571,29 +578,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()
@ -606,7 +600,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()
@ -649,10 +643,10 @@ internal class SendViewModel @Inject constructor(
val isRecent = type == EnterAddressSource.RecentAddress
if (isRecent && isValidAddress) onNextClick()
}
// endregion
// endregion
// region fee
override fun feeReload(isToNextState: Boolean) = loadFee(isToNextState = isToNextState)
override fun feeReload() = loadFee()
override fun onFeeSelectorClick(feeType: FeeType) {
uiState = feeStateFactory.onFeeSelectedState(feeType)
@ -667,12 +661,6 @@ internal class SendViewModel @Inject constructor(
updateFeeNotifications()
}
override fun onSubtractSelect() {
uiState = stateFactory.onSubtractSelect(isAmountSubtractAvailable)
stateRouter.showSend()
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(true))
}
override fun onReadMoreClick() {
val locale = if (Locale.getDefault().language == RU_LOCALE) RU_LOCALE else EN_LOCALE
val url = buildString {
@ -683,10 +671,7 @@ internal class SendViewModel @Inject constructor(
innerRouter.openUrl(url)
}
private fun loadFee(isToNextState: Boolean = false) {
// debouncing fee request
if (SystemClock.elapsedRealtime() - feeIdleTimer < FEE_UPDATE_DELAY) return
private fun loadFee() {
viewModelScope.launch(dispatchers.main) {
val isShowStatus = uiState.feeState?.fee == null
if (isShowStatus) {
@ -694,36 +679,23 @@ internal class SendViewModel @Inject constructor(
}
val result = callFeeUseCase()?.fold(
ifRight = {
feeIdleTimer = SystemClock.elapsedRealtime()
uiState = feeStateFactory.onFeeOnLoadedState(it)
if (isToNextState && !onFeeCoverageAlert()) {
stateRouter.showSend()
}
sendIdleTimer = SystemClock.elapsedRealtime()
},
ifLeft = {
onFeeLoadFailed(isShowStatus, isToNextState)
onFeeLoadFailed(isShowStatus)
},
)
if (result == null) {
onFeeLoadFailed(isShowStatus, isToNextState)
onFeeLoadFailed(isShowStatus)
}
updateNotifications()
updateFeeNotifications()
}.saveIn(feeJobHolder)
.invokeOnCompletion {
uiState = amountStateFactory.getOnAmountFeeLoadingCancel()
}
}
private fun onFeeLoadFailed(isShowStatus: Boolean, isToNextState: Boolean) {
when {
isToNextState -> {
uiState = eventStateFactory.getFeeUnreachableErrorState {
uiState = eventStateFactory.onConsumeEventState()
}
}
isShowStatus -> uiState = feeStateFactory.onFeeOnErrorState()
}
private fun onFeeLoadFailed(isShowStatus: Boolean) {
if (isShowStatus) uiState = feeStateFactory.onFeeOnErrorState()
}
private suspend fun checkIfSubtractAvailable() {
@ -738,14 +710,16 @@ internal class SendViewModel @Inject constructor(
val recipientState = uiState.recipientState ?: return null
val amount = amountState.amountTextField.cryptoAmount.value ?: return null
return getFeeUseCase.invoke(
amount = amount,
destination = recipientState.addressTextField.value,
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
)
return feeCryptoCurrencyStatus?.let { feeCurrencyStatus ->
getFeeUseCase.invoke(
amount = amount,
destination = recipientState.addressTextField.value,
userWallet = userWallet,
cryptoCurrency = feeCurrencyStatus.currency,
)
}
}
// endregion
// endregion
// region send state clicks
override fun onSendClick() {
@ -757,7 +731,6 @@ internal class SendViewModel @Inject constructor(
verifyAndSendTransaction()
} else {
onCheckFeeUpdate()
feeIdleTimer = SystemClock.elapsedRealtime()
sendIdleTimer = SystemClock.elapsedRealtime()
}
}
@ -798,7 +771,7 @@ internal class SendViewModel @Inject constructor(
override fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class<out SendNotification>) {
uiState = amountStateFactory.getOnAmountValueChange(reducedAmount.parseBigDecimal(cryptoCurrency.decimals))
uiState = sendNotificationFactory.dismissNotificationState(clazz)
updateNotifications()
feeReload()
}
override fun onNotificationCancel(clazz: Class<out SendNotification>) {
@ -811,10 +784,18 @@ internal class SendViewModel @Inject constructor(
val fee = feeState.fee ?: return
val memo = uiState.recipientState?.memoTextField?.value
val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return
val feeValue = fee.amount.value ?: return
val receivingAmount = checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
)
viewModelScope.launch(dispatchers.main) {
createTransactionUseCase(
amount = amountValue.convertToAmount(cryptoCurrency),
amount = receivingAmount.convertToAmount(cryptoCurrency),
fee = fee,
memo = memo,
destination = recipient,
@ -923,11 +904,10 @@ internal class SendViewModel @Inject constructor(
}
uiState = stateFactory.getHiddenTapHelpState()
}
// endregion
// endregion
private companion object {
const val CHECK_FEE_UPDATE_DELAY = 60_000L
const val FEE_UPDATE_DELAY = 10_000L
const val BALANCE_UPDATE_DELAY = 10_000L
const val RU_LOCALE = "ru"

View file

@ -433,7 +433,7 @@ internal class StateBuilder(
SwapWarning.NeedReserveToCreateAccount(
notificationConfig = createActivateAccountNotificationConfig(
status.amountToCreateAccount,
quoteModel.toTokenInfo.cryptoCurrencyStatus.currency.name,
quoteModel.toTokenInfo.cryptoCurrencyStatus.currency.symbol,
),
),
)

View file

@ -35,7 +35,7 @@ internal class TxHistoryItemStateConverter(
title = item.extractTitle(),
subtitle = item.extractSubtitle(),
timestamp = item.timestampInMillis,
onClick = { clickIntents.onVisaTransactionClick(item.txHash) },
onClick = { clickIntents.onTransactionClick(item.txHash) },
)
}

View file

@ -85,7 +85,7 @@ leakcanary = "2.13"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "release-app_5.9-605"
tangemBlockchainSdk = "release-app_5.9-608"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "release-app_5.9-343"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^

View file

@ -33,8 +33,15 @@ object BlockchainUtils {
return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet
}
/** If current [networkId] is Dogecoin */
fun isDogecoin(networkId: String): Boolean {
val blockchain = Blockchain.fromId(networkId)
return blockchain == Blockchain.Dogecoin
}
/** If current [networkId] is Tezos */
fun isTezos(networkId: String): Boolean {
val blockchain = Blockchain.fromId(networkId)
return blockchain == Blockchain.Tezos
}
}