Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-16 17:37:13 +03:00
commit bfc4bcb30f
55 changed files with 637 additions and 183 deletions

View file

@ -8,6 +8,8 @@ import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -77,4 +79,10 @@ internal object CardDomainModule {
): GetExtendedPublicKeyForCurrencyUseCase { ): GetExtendedPublicKeyForCurrencyUseCase {
return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository) return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository)
} }
@Provides
@ViewModelScoped
fun provideDeleteSavedAccessCodesUseCase(tangemSdkManager: TangemSdkManager): DeleteSavedAccessCodesUseCase {
return DefaultDeleteSavedAccessCodesUseCase(tangemSdkManager)
}
} }

View file

@ -351,4 +351,12 @@ internal object TokensDomainModule {
): RunPolkadotAccountHealthCheckUseCase { ): RunPolkadotAccountHealthCheckUseCase {
return RunPolkadotAccountHealthCheckUseCase(repository) return RunPolkadotAccountHealthCheckUseCase(repository)
} }
@Provides
@ViewModelScoped
fun provideGetNetworkStatusesUseCase(networksRepository: NetworksRepository): GetNetworkAddressesUseCase {
return GetNetworkAddressesUseCase(
networksRepository = networksRepository,
)
}
} }

View file

@ -0,0 +1,22 @@
package com.tangem.tap.domain.card
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
import com.tangem.tap.domain.TangemSdkManager
internal class DefaultDeleteSavedAccessCodesUseCase(
private val tangemSdkManager: TangemSdkManager,
) : DeleteSavedAccessCodesUseCase {
override suspend fun invoke(cardId: String): Either<Throwable, Unit> {
tangemSdkManager.deleteSavedUserCodes(setOf(cardId))
.doOnFailure { return it.left() }
.doOnSuccess { return Unit.right() }
return Unit.right()
}
}

View file

@ -8,15 +8,20 @@ internal class DelegatedKeystoreManager(
private val keystoreManagerProvider: Provider<KeystoreManager>, private val keystoreManagerProvider: Provider<KeystoreManager>,
) : KeystoreManager { ) : KeystoreManager {
override suspend fun get(masterKeyConfig: KeystoreManager.MasterKeyConfig, keyAlias: String): SecretKey? { override suspend fun get(
return keystoreManagerProvider().get(masterKeyConfig, keyAlias) masterKeyConfig: KeystoreManager.MasterKeyConfig,
keyAlias: String,
forceAuthentication: Boolean,
): SecretKey? {
return keystoreManagerProvider().get(masterKeyConfig, keyAlias, forceAuthentication)
} }
override suspend fun get( override suspend fun get(
masterKeyConfig: KeystoreManager.MasterKeyConfig, masterKeyConfig: KeystoreManager.MasterKeyConfig,
keyAliases: Set<String>, keyAliases: Set<String>,
forceAuthentication: Boolean,
): Map<String, SecretKey> { ): Map<String, SecretKey> {
return keystoreManagerProvider().get(masterKeyConfig, keyAliases) return keystoreManagerProvider().get(masterKeyConfig, keyAliases, forceAuthentication)
} }
override suspend fun store(masterKeyConfig: KeystoreManager.MasterKeyConfig, keyAlias: String, key: SecretKey) { override suspend fun store(masterKeyConfig: KeystoreManager.MasterKeyConfig, keyAlias: String, key: SecretKey) {

View file

@ -11,6 +11,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.tap.features.main.model.MainScreenState import com.tangem.tap.features.main.model.MainScreenState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -26,6 +27,7 @@ internal class MainViewModel @Inject constructor(
private val reduxNavController: ReduxNavController, private val reduxNavController: ReduxNavController,
private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase, private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase,
private val deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase, private val deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase,
private val sendFeatureToggles: SendFeatureToggles,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel(), MainIntents { ) : ViewModel(), MainIntents {
@ -41,6 +43,7 @@ internal class MainViewModel @Inject constructor(
init { init {
updateAppCurrencies() updateAppCurrencies()
updateSendFeatureToggle()
observeFlips() observeFlips()
displayBalancesHidingStatusToast() displayBalancesHidingStatusToast()
displayHiddenBalancesModalNotification() displayHiddenBalancesModalNotification()
@ -56,6 +59,12 @@ internal class MainViewModel @Inject constructor(
} }
} }
private fun updateSendFeatureToggle() {
viewModelScope.launch(dispatchers.main) {
sendFeatureToggles.fetchNewSendEnabled()
}
}
private fun observeFlips() { private fun observeFlips() {
listenToFlipsUseCase().launchIn(viewModelScope) listenToFlipsUseCase().launchIn(viewModelScope)
} }

View file

@ -97,10 +97,11 @@ sealed class AnalyticsParam {
} }
sealed class FeeType(val value: String) { sealed class FeeType(val value: String) {
object Fixed : FeeType("Fixed") data object Fixed : FeeType("Fixed")
object Min : FeeType("Min") data object Min : FeeType("Min")
object Normal : FeeType("Normal") data object Normal : FeeType("Normal")
object Max : FeeType("Max") data object Max : FeeType("Max")
data object Custom : FeeType("Custom")
companion object { companion object {
fun fromString(feeType: String): FeeType { fun fromString(feeType: String): FeeType {

View file

@ -82,4 +82,7 @@ interface TangemTechApi {
@Header("card_id") cardId: String, @Header("card_id") cardId: String,
@Body body: CreateUserNetworkAccountBody, @Body body: CreateUserNetworkAccountBody,
): ApiResponse<CreateUserNetworkAccountResponse> ): ApiResponse<CreateUserNetworkAccountResponse>
@GET("features")
suspend fun getFeatures(): ApiResponse<FeaturesResponse>
} }

View file

@ -0,0 +1,7 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
data class FeaturesResponse(
@Json(name = "send") val isNewSendEnabled: Boolean,
)

View file

@ -17,7 +17,7 @@
}, },
{ {
"name": "LOCAL_USER_LOGS_ENABLED", "name": "LOCAL_USER_LOGS_ENABLED",
"version": "5.10.0" "version": "5.11.0"
}, },
{ {
"name": "GENERATE_XPUB_ENABLED", "name": "GENERATE_XPUB_ENABLED",

View file

@ -102,6 +102,7 @@
<string name="common_locked">Заблокирован</string> <string name="common_locked">Заблокирован</string>
<string name="common_main_network">Основная сеть</string> <string name="common_main_network">Основная сеть</string>
<string name="common_network_fee_title">Сетевая комиссия</string> <string name="common_network_fee_title">Сетевая комиссия</string>
<string name="common_network_fee_warning_content">Сумма отправки будет уменьшена на %1$s (%2$s) для покрытия выбранного уровня комиссии</string>
<string name="common_next">Далее</string> <string name="common_next">Далее</string>
<string name="common_no">Нет</string> <string name="common_no">Нет</string>
<string name="common_no_address">Нет адреса</string> <string name="common_no_address">Нет адреса</string>
@ -448,11 +449,13 @@
<string name="send_alert_transaction_failed_text">Причина: %1$s\nКод: %2$s</string> <string name="send_alert_transaction_failed_text">Причина: %1$s\nКод: %2$s</string>
<string name="send_alert_transaction_failed_title">Транзакция не выполнена</string> <string name="send_alert_transaction_failed_title">Транзакция не выполнена</string>
<string name="send_amount_label">Сумма</string> <string name="send_amount_label">Сумма</string>
<string name="send_bitcoin_custom_fee_footer">Вы можете установить комиссию за транзакцию, изменив значение в поле Satoshi per vByte.</string>
<string name="send_date_format">%1$s, %2$s</string> <string name="send_date_format">%1$s, %2$s</string>
<string name="send_destination_hint_address">Адрес</string> <string name="send_destination_hint_address">Адрес</string>
<string name="send_destination_tag_field">Код назначения</string> <string name="send_destination_tag_field">Код назначения</string>
<string name="send_enter_address_field">Введите адрес</string> <string name="send_enter_address_field">Введите адрес</string>
<string name="send_error_address_same_as_wallet">Адрес совпадает с адресом кошелька</string> <string name="send_error_address_same_as_wallet">Адрес совпадает с адресом кошелька</string>
<string name="send_evm_custom_fee_footer">Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение.</string>
<string name="send_extras_error_invalid_destination_tag">Недопустимый Tag. Он не будет добавлен в транзакцию.</string> <string name="send_extras_error_invalid_destination_tag">Недопустимый Tag. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_error_invalid_memo">Недопустимый Memo. Он не будет добавлен в транзакцию.</string> <string name="send_extras_error_invalid_memo">Недопустимый Memo. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_hint_destination_tag">Tag</string> <string name="send_extras_hint_destination_tag">Tag</string>
@ -477,12 +480,12 @@
<string name="send_network_fee_warning_title">Покрытие сетевой комиссии</string> <string name="send_network_fee_warning_title">Покрытие сетевой комиссии</string>
<string name="send_notification_exceed_balance_text">Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса</string> <string name="send_notification_exceed_balance_text">Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса</string>
<string name="send_notification_exceed_balance_title">Недостаточно средств</string> <string name="send_notification_exceed_balance_title">Недостаточно средств</string>
<string name="send_notification_existential_deposit_button">Оставить %s</string> <string name="send_notification_leave_button">Оставить %s</string>
<string name="send_notification_existential_deposit_text">Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, оставьте %s на балансе.</string> <string name="send_notification_existential_deposit_text">Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, оставьте %s на балансе.</string>
<string name="send_notification_existential_deposit_title">Экзистенциальный депозит</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_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
<string name="send_notification_fee_too_high_title">Установлена высокая комиссия</string> <string name="send_notification_fee_too_high_title">Установлена высокая комиссия</string>
<string name="send_notification_high_fee_text">Ввиду особенности сети Tezos комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s.</string> <string name="send_notification_high_fee_text">Ввиду особенности сети %1$s комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %2$s.</string>
<string name="send_notification_high_fee_title">Комиссия повышена</string> <string name="send_notification_high_fee_title">Комиссия повышена</string>
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению</string> <string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению</string>
<string name="send_notification_invalid_amount_title">Недопустимая сумма</string> <string name="send_notification_invalid_amount_title">Недопустимая сумма</string>
@ -490,7 +493,6 @@
<string name="send_notification_invalid_reserve_amount_text">Адрес получателя не активирован. \nПожалуйста, измените сумму отправки, чтобы продолжить.</string> <string name="send_notification_invalid_reserve_amount_text">Адрес получателя не активирован. \nПожалуйста, измените сумму отправки, чтобы продолжить.</string>
<string name="send_notification_invalid_reserve_amount_title">Сумма отправки не может быть менее %s</string> <string name="send_notification_invalid_reserve_amount_title">Сумма отправки не может быть менее %s</string>
<string name="send_notification_reduce_by">Уменьшить на %s</string> <string name="send_notification_reduce_by">Уменьшить на %s</string>
<string name="send_notification_reduce_to">Уменьшить до %s</string>
<string name="send_notification_transaction_delay_text">Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции</string> <string name="send_notification_transaction_delay_text">Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции</string>
<string name="send_notification_transaction_delay_title">Возможны задержки по транзакции</string> <string name="send_notification_transaction_delay_title">Возможны задержки по транзакции</string>
<string name="send_notification_transaction_limit_text">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string> <string name="send_notification_transaction_limit_text">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string>
@ -500,7 +502,7 @@
<string name="send_recent_transactions">Последние</string> <string name="send_recent_transactions">Последние</string>
<string name="send_recipient">Получатель</string> <string name="send_recipient">Получатель</string>
<string name="send_recipient_address_error">Неверный адрес</string> <string name="send_recipient_address_error">Неверный адрес</string>
<string name="send_recipient_address_footer">Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов</string> <string name="send_recipient_address_footer">Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов.</string>
<string name="send_recipient_label">Отправить</string> <string name="send_recipient_label">Отправить</string>
<string name="send_recipient_memo_footer">Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств.</string> <string name="send_recipient_memo_footer">Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств.</string>
<string name="send_recipient_wallets_title">Мои кошельки</string> <string name="send_recipient_wallets_title">Мои кошельки</string>
@ -509,7 +511,7 @@
<string name="send_sending">Отправка</string> <string name="send_sending">Отправка</string>
<string name="send_summary_tap_hint">Нажмите на любое поле, чтобы изменить его</string> <string name="send_summary_tap_hint">Нажмите на любое поле, чтобы изменить его</string>
<string name="send_summary_title">Отправка %s</string> <string name="send_summary_title">Отправка %s</string>
<string name="send_summary_transaction_description">Вы отправляете %1$s, включая комиссию сети %2$s</string> <string name="send_summary_transaction_description">Вы отправляете **%1$s**, включая комиссию сети %2$s</string>
<string name="send_title_currency_format">Отправка %s</string> <string name="send_title_currency_format">Отправка %s</string>
<string name="send_total_label">Всего</string> <string name="send_total_label">Всего</string>
<string name="send_total_subtitle_asset_format">%1$s и %2$s будет отправлено</string> <string name="send_total_subtitle_asset_format">%1$s и %2$s будет отправлено</string>
@ -643,6 +645,8 @@
<string name="warning_access_denied_message">Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку</string> <string name="warning_access_denied_message">Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку</string>
<string name="warning_backup_errors_message">Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян.</string> <string name="warning_backup_errors_message">Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян.</string>
<string name="warning_backup_errors_title">Ошибка активации</string> <string name="warning_backup_errors_title">Ошибка активации</string>
<string name="warning_beacon_chain_retirement_content">По решению разработчиков сети BNB, стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять свои активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении чтобы перевести их в cеть BNB Smart Chain.</string>
<string name="warning_beacon_chain_retirement_title">Отключение сети BNB Beacon Chain</string>
<string name="warning_button_could_be_better">Можно лучше</string> <string name="warning_button_could_be_better">Можно лучше</string>
<string name="warning_button_like_it">Нравится</string> <string name="warning_button_like_it">Нравится</string>
<string name="warning_button_ok">Понятно!</string> <string name="warning_button_ok">Понятно!</string>

View file

@ -101,6 +101,7 @@
<string name="common_locked">Locked</string> <string name="common_locked">Locked</string>
<string name="common_main_network">Main network</string> <string name="common_main_network">Main network</string>
<string name="common_network_fee_title">Network fee</string> <string name="common_network_fee_title">Network fee</string>
<string name="common_network_fee_warning_content">Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level</string>
<string name="common_next">Next</string> <string name="common_next">Next</string>
<string name="common_no">No</string> <string name="common_no">No</string>
<string name="common_no_address">No address</string> <string name="common_no_address">No address</string>
@ -445,11 +446,13 @@
<string name="send_amount_label">Amount</string> <string name="send_amount_label">Amount</string>
<string name="send_base_fee">Base fee</string> <string name="send_base_fee">Base fee</string>
<string name="send_base_fee_footer">Represents the part of the transaction fee that goes to the miner</string> <string name="send_base_fee_footer">Represents the part of the transaction fee that goes to the miner</string>
<string name="send_bitcoin_custom_fee_footer">You can set your transaction fee by adjusting the value in the Satoshi per vByte field.</string>
<string name="send_date_format">%1$s, %2$s</string> <string name="send_date_format">%1$s, %2$s</string>
<string name="send_destination_hint_address">Address</string> <string name="send_destination_hint_address">Address</string>
<string name="send_destination_tag_field">Destination Tag</string> <string name="send_destination_tag_field">Destination Tag</string>
<string name="send_enter_address_field">Enter address</string> <string name="send_enter_address_field">Enter address</string>
<string name="send_error_address_same_as_wallet">Address is the same as wallet address</string> <string name="send_error_address_same_as_wallet">Address is the same as wallet address</string>
<string name="send_evm_custom_fee_footer">The fee that will be charged for your transaction. You can set your own value.</string>
<string name="send_extras_error_invalid_destination_tag">Invalid Tag. It won\'t be added to the transaction.</string> <string name="send_extras_error_invalid_destination_tag">Invalid Tag. It won\'t be added to the transaction.</string>
<string name="send_extras_error_invalid_memo">Invalid Memo. It won\'t be added to the transaction.</string> <string name="send_extras_error_invalid_memo">Invalid Memo. It won\'t be added to the transaction.</string>
<string name="send_extras_hint_destination_tag">Tag</string> <string name="send_extras_hint_destination_tag">Tag</string>
@ -474,20 +477,19 @@
<string name="send_network_fee_warning_title">Network fee coverage</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_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_exceed_balance_title">Total exceeds balance</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_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_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_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_fee_too_high_title">Custom fee is high</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_text">Due to the peculiarities of the %1$s network, the fee for transferring the entire balance is higher. To reduce the commission, you can leave %2$s.</string>
<string name="send_notification_high_fee_title">The fee is higher</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_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_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> <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>
<string name="send_notification_invalid_reserve_amount_text">Target account is not created. Please change the amount to send.</string> <string name="send_notification_invalid_reserve_amount_text">Target account is not created. Please change the amount to send.</string>
<string name="send_notification_invalid_reserve_amount_title">The amount to send must be at least %s</string> <string name="send_notification_invalid_reserve_amount_title">The amount to send must be at least %s</string>
<string name="send_notification_leave_button">Leave %s</string>
<string name="send_notification_reduce_by">Reduce by %s</string> <string name="send_notification_reduce_by">Reduce by %s</string>
<string name="send_notification_reduce_to">Reduce to %s</string>
<string name="send_notification_transaction_delay_text">Kindly be aware that your transaction may experience delays under specific fee settings</string> <string name="send_notification_transaction_delay_text">Kindly be aware that your transaction may experience delays under specific fee settings</string>
<string name="send_notification_transaction_delay_title">Transaction delays are possible</string> <string name="send_notification_transaction_delay_title">Transaction delays are possible</string>
<string name="send_notification_transaction_limit_text">Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.</string> <string name="send_notification_transaction_limit_text">Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.</string>
@ -499,7 +501,7 @@
<string name="send_recent_transactions">Recent</string> <string name="send_recent_transactions">Recent</string>
<string name="send_recipient">Recipient</string> <string name="send_recipient">Recipient</string>
<string name="send_recipient_address_error">Not a valid address</string> <string name="send_recipient_address_error">Not a valid address</string>
<string name="send_recipient_address_footer">Ensure that you are sending funds to an %s wallet address. Errors may result in the loss of your tokens</string> <string name="send_recipient_address_footer">Ensure the receiving wallet address is on the %s network to avoid losing your tokens</string>
<string name="send_recipient_label">Send to</string> <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_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_recipient_wallets_title">My wallets</string>
@ -508,7 +510,7 @@
<string name="send_sending">Sending...</string> <string name="send_sending">Sending...</string>
<string name="send_summary_tap_hint">Tap any field to change it</string> <string name="send_summary_tap_hint">Tap any field to change it</string>
<string name="send_summary_title">Send %s</string> <string name="send_summary_title">Send %s</string>
<string name="send_summary_transaction_description">You are sending %1$s including a network fee of %2$s</string> <string name="send_summary_transaction_description">You are sending **%1$s** including a network fee of %2$s</string>
<string name="send_title_currency_format">Sending %s</string> <string name="send_title_currency_format">Sending %s</string>
<string name="send_total_label">Total</string> <string name="send_total_label">Total</string>
<string name="send_total_subtitle_asset_format">%1$s and %2$s will be sent</string> <string name="send_total_subtitle_asset_format">%1$s and %2$s will be sent</string>
@ -643,6 +645,8 @@
<string name="warning_access_denied_message">Use %s or scan a card to unlock access to your wallet</string> <string name="warning_access_denied_message">Use %s or scan a card to unlock access to your wallet</string>
<string name="warning_backup_errors_message">Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost.</string> <string name="warning_backup_errors_message">Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost.</string>
<string name="warning_backup_errors_title">Activation error</string> <string name="warning_backup_errors_title">Activation error</string>
<string name="warning_beacon_chain_retirement_content">According to BNB network developers, support for the BEP-2 standard will end in June 2024. To avoid losing assets with this standard, please convert them to the BEP-20 standard. Use our swap service to transfer them to the BNB Smart Chain network.</string>
<string name="warning_beacon_chain_retirement_title">BNB Beacon Chain will shut down</string>
<string name="warning_button_could_be_better">Could be better</string> <string name="warning_button_could_be_better">Could be better</string>
<string name="warning_button_like_it">Like it</string> <string name="warning_button_like_it">Like it</string>
<string name="warning_button_ok">Ok, Got it!</string> <string name="warning_button_ok">Ok, Got it!</string>

View file

@ -45,4 +45,5 @@ dependencies {
implementation(deps.zxing.qrCore) implementation(deps.zxing.qrCore)
implementation(deps.jodatime) implementation(deps.jodatime)
implementation(deps.timber) implementation(deps.timber)
implementation(deps.markdown)
} }

View file

@ -94,10 +94,14 @@ fun AmountTextField(
SimpleTextField( SimpleTextField(
value = value, value = value,
onValueChange = { newText -> onValueChange = { newText ->
if (decimalFormat.isValidSymbols(newText)) { onValueChange(
val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals) prepareEnter(
onValueChange(trimmed) oldValue = value,
} newValue = newText,
decimalFormat = decimalFormat,
decimals = decimals,
),
)
}, },
textStyle = textStyle.copy( textStyle = textStyle.copy(
fontSize = fontSize, fontSize = fontSize,
@ -116,8 +120,37 @@ fun AmountTextField(
} }
} }
private fun prepareEnter(oldValue: String, newValue: String, decimalFormat: DecimalFormat, decimals: Int): String {
val decimalSymbol = decimalFormat.decimalFormatSymbols.decimalSeparator
return if (decimalFormat.isValidSymbols(newValue)) {
val parsedValue = newValue.parseBigDecimalOrNull()?.toPlainString()
?: if (newValue.isBlank()) "" else oldValue
val replacedWithSymbol = if (parsedValue.findLast { it != decimalSymbol } != null) {
when {
parsedValue.findLast { it == COMMA_SEPARATOR } != null -> {
parsedValue.replace(COMMA_SEPARATOR, decimalSymbol)
}
parsedValue.findLast { it == POINT_SEPARATOR } != null -> {
parsedValue.replace(POINT_SEPARATOR, decimalSymbol)
}
else -> parsedValue
}
} else {
parsedValue
}
val joinedSymbol = if (newValue.endsWith(COMMA_SEPARATOR) || newValue.endsWith(POINT_SEPARATOR)) {
replacedWithSymbol.plus(decimalSymbol)
} else {
replacedWithSymbol
}
decimalFormat.getValidatedNumberWithFixedDecimals(joinedSymbol, decimals)
} else {
oldValue
}
}
private fun DecimalFormat.isValidSymbols(text: String): Boolean { private fun DecimalFormat.isValidSymbols(text: String): Boolean {
return checkDecimalSeparatorDuplicate(text) && checkGroupingSeparator(text) return checkDecimalSeparatorDuplicate(text)
} }
// region preview // region preview

View file

@ -38,6 +38,7 @@ fun SimpleTextField(
keyboardActions: KeyboardActions = KeyboardActions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default,
color: Color = TangemTheme.colors.text.primary1, color: Color = TangemTheme.colors.text.primary1,
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color), textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
placeholderColor: Color = TangemTheme.colors.text.disabled,
readOnly: Boolean = false, readOnly: Boolean = false,
isValuePasted: Boolean = false, isValuePasted: Boolean = false,
onValuePastedTriggerDismiss: () -> Unit = {}, onValuePastedTriggerDismiss: () -> Unit = {},
@ -108,6 +109,7 @@ fun SimpleTextField(
value = value, value = value,
textStyle = textStyle, textStyle = textStyle,
textValue = textValue, textValue = textValue,
color = placeholderColor,
) )
}, },
modifier = modifier modifier = modifier
@ -122,6 +124,7 @@ private fun SimpleTextPlaceholder(
value: String, value: String,
textStyle: TextStyle, textStyle: TextStyle,
textValue: @Composable () -> Unit, textValue: @Composable () -> Unit,
color: Color = TangemTheme.colors.text.disabled,
) { ) {
Box { Box {
if (value.isBlank() && placeholder != null) { if (value.isBlank() && placeholder != null) {
@ -132,7 +135,7 @@ private fun SimpleTextPlaceholder(
Text( Text(
text = it.resolveReference(), text = it.resolveReference(),
style = textStyle, style = textStyle,
color = TangemTheme.colors.text.disabled, color = color,
) )
} }
} }

View file

@ -49,12 +49,14 @@ fun Notification(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
containerColor: Color? = null, containerColor: Color? = null,
iconTint: Color? = null, iconTint: Color? = null,
isEnabled: Boolean = true,
) { ) {
BaseContainer( BaseContainer(
buttonsState = config.buttonsState, buttonsState = config.buttonsState,
onClick = config.onClick, onClick = config.onClick,
modifier = modifier, modifier = modifier,
containerColor = containerColor, containerColor = containerColor,
isEnabled = isEnabled,
) { ) {
Column( Column(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
@ -65,15 +67,16 @@ fun Notification(
iconTint = iconTint, iconTint = iconTint,
title = config.title, title = config.title,
subtitle = config.subtitle, subtitle = config.subtitle,
isClickableComponent = config.onClick != null, isClickableComponent = isEnabled && config.onClick != null,
) )
Buttons(state = config.buttonsState) Buttons(state = config.buttonsState, isEnabled = isEnabled)
} }
CloseableIconButton( CloseableIconButton(
onClick = config.onCloseClick, onClick = config.onCloseClick,
modifier = Modifier.align(alignment = Alignment.TopEnd), modifier = Modifier.align(alignment = Alignment.TopEnd),
isEnabled = isEnabled,
) )
} }
} }
@ -83,6 +86,7 @@ private fun BaseContainer(
buttonsState: NotificationConfig.ButtonsState?, buttonsState: NotificationConfig.ButtonsState?,
onClick: (() -> Unit)?, onClick: (() -> Unit)?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isEnabled: Boolean = true,
containerColor: Color? = null, containerColor: Color? = null,
content: @Composable BoxScope.() -> Unit, content: @Composable BoxScope.() -> Unit,
) { ) {
@ -99,7 +103,7 @@ private fun BaseContainer(
modifier = modifier modifier = modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size62) .defaultMinSize(minHeight = TangemTheme.dimens.size62)
.fillMaxWidth(), .fillMaxWidth(),
enabled = onClick != null, enabled = onClick != null && isEnabled,
shape = TangemTheme.shapes.roundedCornersXMedium, shape = TangemTheme.shapes.roundedCornersXMedium,
color = containerColor ?: tempContainerColor, color = containerColor ?: tempContainerColor,
) { ) {
@ -177,27 +181,31 @@ private fun TextsBlock(title: TextReference, subtitle: TextReference) {
} }
@Composable @Composable
private fun Buttons(state: NotificationButtonsState?) { private fun Buttons(state: NotificationButtonsState?, isEnabled: Boolean = true) {
when (state) { when (state) {
is NotificationButtonsState.SecondaryButtonConfig -> SingleSecondaryButton(config = state) is NotificationButtonsState.SecondaryButtonConfig -> SingleSecondaryButton(
is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = state) config = state,
is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = state) isEnabled = isEnabled,
)
is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = state, isEnabled = isEnabled)
is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = state, isEnabled = isEnabled)
null -> Unit null -> Unit
} }
} }
@Composable @Composable
private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButtonConfig) { private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButtonConfig, isEnabled: Boolean = true) {
SecondaryButton( SecondaryButton(
text = config.text.resolveReference(), text = config.text.resolveReference(),
onClick = config.onClick, onClick = config.onClick,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
size = TangemButtonSize.WideAction, size = TangemButtonSize.WideAction,
enabled = isEnabled,
) )
} }
@Composable @Composable
private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonConfig) { private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonConfig, isEnabled: Boolean = true) {
if (config.iconResId != null) { if (config.iconResId != null) {
PrimaryButtonIconEnd( PrimaryButtonIconEnd(
text = config.text.resolveReference(), text = config.text.resolveReference(),
@ -205,6 +213,7 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo
onClick = config.onClick, onClick = config.onClick,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
size = TangemButtonSize.WideAction, size = TangemButtonSize.WideAction,
enabled = isEnabled,
) )
} else { } else {
PrimaryButton( PrimaryButton(
@ -212,18 +221,20 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo
onClick = config.onClick, onClick = config.onClick,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
size = TangemButtonSize.WideAction, size = TangemButtonSize.WideAction,
enabled = isEnabled,
) )
} }
} }
@Composable @Composable
private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig) { private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig, isEnabled: Boolean = true) {
Row(horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8)) { Row(horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8)) {
SecondaryButton( SecondaryButton(
text = config.secondaryText.resolveReference(), text = config.secondaryText.resolveReference(),
onClick = config.onSecondaryClick, onClick = config.onSecondaryClick,
modifier = Modifier.weight(weight = 1f), modifier = Modifier.weight(weight = 1f),
size = TangemButtonSize.WideAction, size = TangemButtonSize.WideAction,
enabled = isEnabled,
) )
PrimaryButton( PrimaryButton(
@ -231,12 +242,13 @@ private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig) {
onClick = config.onPrimaryClick, onClick = config.onPrimaryClick,
modifier = Modifier.weight(weight = 1f), modifier = Modifier.weight(weight = 1f),
size = TangemButtonSize.WideAction, size = TangemButtonSize.WideAction,
enabled = isEnabled,
) )
} }
} }
@Composable @Composable
private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) { private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier, isEnabled: Boolean = true) {
AnimatedVisibility(visible = onClick != null, modifier = modifier) { AnimatedVisibility(visible = onClick != null, modifier = modifier) {
onClick ?: return@AnimatedVisibility onClick ?: return@AnimatedVisibility
@ -253,6 +265,7 @@ private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Mod
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = LocalIndication.current, indication = LocalIndication.current,
role = Role.Button, role = Role.Button,
enabled = isEnabled,
onClick = onClick, onClick = onClick,
), ),
) { ) {

View file

@ -0,0 +1,56 @@
package com.tangem.core.ui.extensions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import org.intellij.markdown.MarkdownElementTypes
import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.ast.getTextInNode
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
import org.intellij.markdown.parser.MarkdownParser
/** Markdown parser */
@Composable
fun rememberMarkdownParser() = remember {
MarkdownParser(CommonMarkFlavourDescriptor())
}
/**
* Styling markdown tree recursively
*
* @param markdownText original text
* @param node current processed node
*/
@Composable
fun AnnotatedString.Builder.appendMarkdown(markdownText: String, node: ASTNode): AnnotatedString.Builder {
when (node.type) {
MarkdownElementTypes.MARKDOWN_FILE, MarkdownElementTypes.PARAGRAPH -> {
node.children.forEach { childNode ->
appendMarkdown(
markdownText = markdownText,
node = childNode,
)
}
}
MarkdownElementTypes.STRONG -> {
withStyle(SpanStyle(fontWeight = FontWeight.Medium)) {
node.children
.drop(2)
.dropLast(2)
.forEach { childNode ->
appendMarkdown(
markdownText = markdownText,
node = childNode,
)
}
}
}
else -> {
append(node.getTextInNode(markdownText).toString())
}
}
return this
}

View file

@ -8,6 +8,9 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import org.intellij.markdown.MarkdownElementTypes
/** /**
* Utility class for creating text as [String] or [StringRes]. * Utility class for creating text as [String] or [StringRes].
@ -160,6 +163,29 @@ fun TextReference.resolveReference(resources: Resources): String {
} }
} }
/** Resolve [TextReference] as [AnnotatedString] */
@Composable
fun TextReference.resolveAnnotatedReference(): AnnotatedString {
return when (this) {
is TextReference.Res -> {
val args = formatArgs
.map { if (it is TextReference) it.resolveReference() else it }
.toTypedArray()
formatAnnotated(stringResource(id = id, *args))
}
is TextReference.PluralRes -> formatAnnotated(
pluralStringResource(id, count, *formatArgs.toTypedArray()),
)
is TextReference.Str -> formatAnnotated(value)
is TextReference.Combined -> buildAnnotatedString {
refs.forEach {
append(formatAnnotated(it.resolveReference()))
}
}
}
}
/** Concatenate [this] reference with [ref] */ /** Concatenate [this] reference with [ref] */
operator fun TextReference.plus(ref: TextReference): TextReference { operator fun TextReference.plus(ref: TextReference): TextReference {
return when (this) { return when (this) {
@ -169,4 +195,14 @@ operator fun TextReference.plus(ref: TextReference): TextReference {
is TextReference.Str, is TextReference.Str,
-> TextReference.Combined(refs = wrappedList(this, ref)) -> TextReference.Combined(refs = wrappedList(this, ref))
} }
}
@Composable
private fun formatAnnotated(rawString: String): AnnotatedString {
val markdownDescriptor = rememberMarkdownParser()
val parsedTree = markdownDescriptor.parse(MarkdownElementTypes.MARKDOWN_FILE, rawString, true)
return buildAnnotatedString {
appendMarkdown(markdownText = rawString, node = parsedTree)
}
} }

View file

@ -10,9 +10,9 @@ import java.text.DecimalFormatSymbols
import java.util.Locale import java.util.Locale
private const val TEXT_CHUNK_THOUSAND = 3 private const val TEXT_CHUNK_THOUSAND = 3
private const val POINT_SEPARATOR = '.'
private const val COMMA_SEPARATOR = ','
private const val SCIENTIFIC_NOTATION = 'e' private const val SCIENTIFIC_NOTATION = 'e'
const val POINT_SEPARATOR = '.'
const val COMMA_SEPARATOR = ','
const val DECIMAL_SEPARATOR_LIMIT = 1 const val DECIMAL_SEPARATOR_LIMIT = 1
@Composable @Composable

View file

@ -0,0 +1,8 @@
package com.tangem.domain.card
import arrow.core.Either
interface DeleteSavedAccessCodesUseCase {
suspend operator fun invoke(cardId: String): Either<Throwable, Unit>
}

View file

@ -25,6 +25,7 @@ dependencies {
/** Project - Other */ /** Project - Other */
implementation(projects.core.utils) implementation(projects.core.utils)
implementation(projects.libs.crypto)
/** Android - Other */ /** Android - Other */
implementation(deps.androidx.paging.runtime) implementation(deps.androidx.paging.runtime)

View file

@ -47,4 +47,6 @@ sealed class CryptoCurrencyWarning {
val startDateTime: DateTime, val startDateTime: DateTime,
val endDateTime: DateTime, val endDateTime: DateTime,
) : CryptoCurrencyWarning() ) : CryptoCurrencyWarning()
data object BeaconChainShutdown : CryptoCurrencyWarning()
} }

View file

@ -12,6 +12,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching import com.tangem.utils.coroutines.runCatching
import com.tangem.utils.isNullOrZero import com.tangem.utils.isNullOrZero
@ -74,6 +75,7 @@ class GetCurrencyWarningsUseCase(
*coinRelatedWarnings.toTypedArray(), *coinRelatedWarnings.toTypedArray(),
getNetworkUnavailableWarning(currencyStatus), getNetworkUnavailableWarning(currencyStatus),
getNetworkNoAccountWarning(currencyStatus), getNetworkNoAccountWarning(currencyStatus),
getBeaconChainShutdownWarning(currency.network.id),
) )
}.flowOn(dispatchers.io) }.flowOn(dispatchers.io)
} }
@ -264,6 +266,10 @@ class GetCurrencyWarningsUseCase(
} }
} }
private fun getBeaconChainShutdownWarning(networkId: Network.ID): CryptoCurrencyWarning.BeaconChainShutdown? {
return if (BlockchainUtils.isBeaconChain(networkId.value)) CryptoCurrencyWarning.BeaconChainShutdown else null
}
private fun BigDecimal?.isZero(): Boolean { private fun BigDecimal?.isZero(): Boolean {
return this?.signum() == 0 return this?.signum() == 0
} }

View file

@ -0,0 +1,24 @@
package com.tangem.domain.tokens
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class GetNetworkAddressesUseCase(
internal val networksRepository: NetworksRepository,
) {
operator fun invoke(userWalletId: UserWalletId, network: Network): Flow<String> =
networksRepository.getNetworkStatusesUpdates(userWalletId, setOf(network))
.map { networkStatuses ->
when (val networkStatus = networkStatuses.singleOrNull { it.network.id == network.id }?.value) {
is NetworkStatus.NoAccount -> networkStatus.address.defaultAddress.value
is NetworkStatus.Unreachable -> networkStatus.address?.defaultAddress?.value.orEmpty()
is NetworkStatus.Verified -> networkStatus.address.defaultAddress.value
else -> ""
}
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.domain.wallets.usecase
import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWallet
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
/** /**
* Use case for getting list of user wallets * Use case for getting list of user wallets
@ -14,7 +15,9 @@ import kotlinx.coroutines.flow.Flow
class GetWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) { class GetWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) {
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
operator fun invoke(): Flow<List<UserWallet>> { operator fun invoke(): Flow<List<UserWallet>> =
return requireNotNull(walletsStateHolder.userWalletsListManager).userWallets requireNotNull(walletsStateHolder.userWalletsListManager).userWallets
}
@Throws(IllegalArgumentException::class)
suspend fun invokeSync(): List<UserWallet>? = walletsStateHolder.userWalletsListManager?.userWallets?.firstOrNull()
} }

View file

@ -7,4 +7,7 @@ interface SendFeatureToggles {
/** Availability of redesigned send screen */ /** Availability of redesigned send screen */
val isRedesignedSendEnabled: Boolean val isRedesignedSendEnabled: Boolean
/** Updates remote toggle */
suspend fun fetchNewSendEnabled()
} }

View file

@ -48,6 +48,7 @@ dependencies {
implementation(projects.core.navigation) implementation(projects.core.navigation)
implementation(projects.core.analytics) implementation(projects.core.analytics)
implementation(projects.core.analytics.models) implementation(projects.core.analytics.models)
implementation(projects.core.datasource)
/** Common */ /** Common */
implementation(projects.common) implementation(projects.common)

View file

@ -1,8 +1,10 @@
package com.tangem.features.send.impl.di package com.tangem.features.send.impl.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.features.send.impl.featuretoggles.DefaultSendFeatureToggles import com.tangem.features.send.impl.featuretoggles.DefaultSendFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -18,7 +20,15 @@ internal object SendFeatureTogglesModule {
@Provides @Provides
@Singleton @Singleton
fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): SendFeatureToggles { fun provideSendFeatureToggles(
return DefaultSendFeatureToggles(featureTogglesManager = featureTogglesManager) featureTogglesManager: FeatureTogglesManager,
tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider,
): SendFeatureToggles {
return DefaultSendFeatureToggles(
featureTogglesManager = featureTogglesManager,
tangemTechApi = tangemTechApi,
dispatchers = dispatchers,
)
} }
} }

View file

@ -1,16 +1,41 @@
package com.tangem.features.send.impl.featuretoggles package com.tangem.features.send.impl.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import timber.log.Timber
/** /**
* Default implementation of Send feature toggles * Default implementation of Send feature toggles
* *
* @property featureTogglesManager manager for getting information about the availability of feature toggles * @property featureTogglesManager manager for getting information about the availability of feature toggles
* @property tangemTechApi api to get remote feature toggle for send
* @property dispatchers coroutine dispatchers
*/ */
internal class DefaultSendFeatureToggles( internal class DefaultSendFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager, private val featureTogglesManager: FeatureTogglesManager,
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) : SendFeatureToggles { ) : SendFeatureToggles {
private val remoteSendEnabled: MutableStateFlow<Boolean> = MutableStateFlow(true)
override val isRedesignedSendEnabled: Boolean override val isRedesignedSendEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_SEND_SCREEN_ENABLED") get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_SEND_SCREEN_ENABLED") &&
remoteSendEnabled.value
override suspend fun fetchNewSendEnabled() {
runCatching(dispatchers.io) {
tangemTechApi.getFeatures().getOrThrow()
}.onSuccess { response ->
remoteSendEnabled.update { response.isNewSendEnabled }
}.onFailure {
Timber.e(it.localizedMessage, "Unable to fetch new send toggle")
}
}
} }

View file

@ -1,7 +1,9 @@
package com.tangem.features.send.impl.presentation.analytics package com.tangem.features.send.impl.presentation.analytics
import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN
import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE
@ -66,9 +68,9 @@ internal sealed class SendAnalyticEvents(
data object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened") data object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened")
/** Selected fee (send after next screen opened) */ /** Selected fee (send after next screen opened) */
data class SelectedFee(val feeType: SelectedFeeType) : SendAnalyticEvents( data class SelectedFee(val feeType: AnalyticsParam.FeeType) : SendAnalyticEvents(
event = "Fee Selected", event = "Fee Selected",
params = mapOf("Fee Type" to feeType.name), params = mapOf("Fee Type" to feeType.value),
) )
/** Custom fee selected */ /** Custom fee selected */
@ -97,7 +99,16 @@ internal sealed class SendAnalyticEvents(
// region Transaction Result // region Transaction Result
/** Transaction send screen opened */ /** Transaction send screen opened */
data object TransactionScreenOpened : SendAnalyticEvents(event = "Transaction Sent Screen Opened") data class TransactionScreenOpened(
val token: String,
val feeType: AnalyticsParam.FeeType,
) : SendAnalyticEvents(
event = "Transaction Sent Screen Opened",
params = mapOf(
TOKEN to token,
FEE_TYPE to feeType.value,
),
)
/** Share button clicked */ /** Share button clicked */
data object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share") data object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share")
@ -145,12 +156,4 @@ internal enum class EnterAddressSource {
internal enum class SelectedCurrencyType(val value: String) { internal enum class SelectedCurrencyType(val value: String) {
Token("Token"), Token("Token"),
AppCurrency("App Currency"), AppCurrency("App Currency"),
}
internal enum class SelectedFeeType {
Min,
Max,
Fixed,
Normal,
Custom,
} }

View file

@ -2,8 +2,10 @@ package com.tangem.features.send.impl.presentation.analytics.utils
import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.send.impl.presentation.analytics.SelectedCurrencyType 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.SendAnalyticEvents
import com.tangem.features.send.impl.presentation.analytics.SendScreenSource 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.SendUiState
@ -11,11 +13,13 @@ 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.StateRouter
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState 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.FeeType
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.utils.Provider import com.tangem.utils.Provider
internal class SendScreenAnalyticSender( internal class SendScreenAnalyticSender(
private val stateRouterProvider: Provider<StateRouter>, private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>, private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyProvider: Provider<CryptoCurrency>,
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
) { ) {
fun send(prevScreen: SendUiStateType, state: SendUiState) { fun send(prevScreen: SendUiStateType, state: SendUiState) {
@ -73,16 +77,57 @@ internal class SendScreenAnalyticSender(
) )
} }
fun sendTransaction() {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val cryptoCurrency = cryptoCurrencyProvider()
val feeState = state.getFeeState(isEditState) ?: return
val recipientState = state.getRecipientState(isEditState) ?: return
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
val feeType = getSendTransactionFeeType(feeSelectorState)
analyticsEventHandler.send(
SendAnalyticEvents.TransactionScreenOpened(
token = cryptoCurrency.symbol,
feeType = feeType,
),
)
analyticsEventHandler.send(
Basic.TransactionSent(
sentFrom = AnalyticsParam.TxSentFrom.Send(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
feeType = feeType,
),
memoType = getSendTransactionMemoType(recipientState.memoTextField),
),
)
}
private fun sendSelectedFeeAnalytics(feeSelectorState: FeeSelectorState.Content) { private fun sendSelectedFeeAnalytics(feeSelectorState: FeeSelectorState.Content) {
val type = when (feeSelectorState.fees) { val type = getSendTransactionFeeType(feeSelectorState)
is TransactionFee.Single -> SelectedFeeType.Fixed
is TransactionFee.Choosable -> when (feeSelectorState.selectedFee) {
FeeType.Slow -> SelectedFeeType.Min
FeeType.Market -> SelectedFeeType.Normal
FeeType.Fast -> SelectedFeeType.Max
FeeType.Custom -> SelectedFeeType.Custom
}
}
analyticsEventHandler.send(SendAnalyticEvents.SelectedFee(type)) analyticsEventHandler.send(SendAnalyticEvents.SelectedFee(type))
} }
private fun getSendTransactionFeeType(feeSelectorState: FeeSelectorState.Content): AnalyticsParam.FeeType =
when (feeSelectorState.fees) {
is TransactionFee.Single -> AnalyticsParam.FeeType.Fixed
is TransactionFee.Choosable -> when (feeSelectorState.selectedFee) {
FeeType.Slow -> AnalyticsParam.FeeType.Min
FeeType.Market -> AnalyticsParam.FeeType.Normal
FeeType.Fast -> AnalyticsParam.FeeType.Max
FeeType.Custom -> AnalyticsParam.FeeType.Custom
}
}
private fun getSendTransactionMemoType(
recipientMemo: SendTextField.RecipientMemo?,
): Basic.TransactionSent.MemoType {
val memo = recipientMemo?.value
return when {
memo?.isBlank() == true -> Basic.TransactionSent.MemoType.Empty
memo?.isNotBlank() == true -> Basic.TransactionSent.MemoType.Full
else -> Basic.TransactionSent.MemoType.Null
}
}
} }

View file

@ -1,15 +1,18 @@
package com.tangem.features.send.impl.presentation.domain package com.tangem.features.send.impl.presentation.domain
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.tangem.domain.wallets.models.UserWalletId
/** /**
* Available wallet to send * Available wallet to send
* *
* @property name wallet name * @property name wallet name
* @property userWalletId wallet id
* @property address blockchain address * @property address blockchain address
*/ */
@Immutable @Immutable
data class AvailableWallet( data class AvailableWallet(
val name: String, val name: String,
val userWalletId: UserWalletId,
val address: String, val address: String,
) )

View file

@ -54,7 +54,7 @@ internal sealed class SendNotification(val config: NotificationConfig) {
wrappedList(cryptoCurrency, utxoLimit, amountLimit), wrappedList(cryptoCurrency, utxoLimit, amountLimit),
), ),
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_reduce_to, wrappedList(amountLimit)), text = resourceReference(R.string.send_notification_leave_button, wrappedList(amountLimit)),
onClick = onConfirmClick, onClick = onConfirmClick,
), ),
) )
@ -98,7 +98,7 @@ internal sealed class SendNotification(val config: NotificationConfig) {
title = resourceReference(R.string.send_notification_existential_deposit_title), title = resourceReference(R.string.send_notification_existential_deposit_title),
subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)),
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_existential_deposit_button, wrappedList(deposit)), text = resourceReference(R.string.send_notification_leave_button, wrappedList(deposit)),
onClick = onConfirmClick, onClick = onConfirmClick,
), ),
) )
@ -119,12 +119,13 @@ internal sealed class SendNotification(val config: NotificationConfig) {
), ),
) { ) {
data class HighFeeError( data class HighFeeError(
val currencyName: String,
val amount: String, val amount: String,
val onConfirmClick: () -> Unit, val onConfirmClick: () -> Unit,
val onCloseClick: () -> Unit, val onCloseClick: () -> Unit,
) : Warning( ) : Warning(
title = resourceReference(R.string.send_notification_high_fee_title), title = resourceReference(R.string.send_notification_high_fee_title),
subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)), subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)),
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)), text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)),
onClick = onConfirmClick, onClick = onConfirmClick,
@ -156,7 +157,7 @@ internal sealed class SendNotification(val config: NotificationConfig) {
data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning( data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning(
title = resourceReference(R.string.send_network_fee_warning_title), title = resourceReference(R.string.send_network_fee_warning_title),
subtitle = resourceReference( subtitle = resourceReference(
R.string.send_network_fee_warning_content, R.string.common_network_fee_warning_content,
wrappedList(cryptoAmount, fiatAmount), wrappedList(cryptoAmount, fiatAmount),
), ),
) )

View file

@ -66,11 +66,12 @@ internal class SendNotificationFactory(
amountValue = amountValue, amountValue = amountValue,
feeValue = feeValue, feeValue = feeValue,
) )
val sendingAmount = calculateSubtractedAmount( val sendingAmount = checkAndCalculateSubtractedAmount(
isFeeCoverage = isFeeCoverage, isAmountSubtractAvailable = isFeeCoverage,
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
amountValue = amountValue, amountValue = amountValue,
feeValue = feeValue, feeValue = feeValue,
reduceAmountBy = sendState.reduceAmountBy,
) )
buildList { buildList {
// errors // errors
@ -194,7 +195,7 @@ internal class SendNotificationFactory(
val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) { val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) {
feeAmount feeAmount
} else { } else {
feeAmount + receivedAmount receivedAmount
} }
val currencyDeposit = currencyChecksRepository.getExistentialDeposit( val currencyDeposit = currencyChecksRepository.getExistentialDeposit(
userWalletId, userWalletId,
@ -260,6 +261,7 @@ internal class SendNotificationFactory(
if (!ignoreAmountReduce && isTotalBalance && isTezos) { if (!ignoreAmountReduce && isTotalBalance && isTezos) {
add( add(
SendNotification.Warning.HighFeeError( SendNotification.Warning.HighFeeError(
currencyName = cryptoCurrencyStatus.currency.name,
amount = threshold.toPlainString(), amount = threshold.toPlainString(),
onConfirmClick = { onConfirmClick = {
clickIntents.onAmountReduceClick( clickIntents.onAmountReduceClick(

View file

@ -54,7 +54,7 @@ internal fun checkFeeCoverage(
/** /**
* Calculates subtracted amount * Calculates subtracted amount
*/ */
internal fun calculateSubtractedAmount( private fun calculateSubtractedAmount(
isFeeCoverage: Boolean, isFeeCoverage: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus, cryptoCurrencyStatus: CryptoCurrencyStatus,
amountValue: BigDecimal, amountValue: BigDecimal,

View file

@ -47,7 +47,7 @@ internal class BitcoinCustomFeeConverter(
keyboardType = KeyboardType.Number, keyboardType = KeyboardType.Number,
), ),
title = resourceReference(R.string.send_max_fee), title = resourceReference(R.string.send_max_fee),
footer = resourceReference(R.string.send_max_fee_footer), footer = resourceReference(R.string.send_bitcoin_custom_fee_footer),
label = getFiatReference( label = getFiatReference(
rate = feeCurrency?.fiatRate, rate = feeCurrency?.fiatRate,
value = feeValue, value = feeValue,

View file

@ -43,7 +43,7 @@ internal class EthereumCustomFeeConverter(
keyboardType = KeyboardType.Number, keyboardType = KeyboardType.Number,
), ),
title = resourceReference(R.string.send_max_fee), title = resourceReference(R.string.send_max_fee),
footer = resourceReference(R.string.send_max_fee_footer), footer = resourceReference(R.string.send_evm_custom_fee_footer),
label = getFiatReference( label = getFiatReference(
rate = feeCurrency?.fiatRate, rate = feeCurrency?.fiatRate,
value = feeValue, value = feeValue,

View file

@ -10,6 +10,7 @@ import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.utils.Provider import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero import com.tangem.utils.isNullOrZero
import java.math.RoundingMode
internal class SendAmountFieldMaxAmountConverter( internal class SendAmountFieldMaxAmountConverter(
private val stateRouterProvider: Provider<StateRouter>, private val stateRouterProvider: Provider<StateRouter>,
@ -33,7 +34,7 @@ internal class SendAmountFieldMaxAmountConverter(
val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero() val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero()
val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty() val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty()
val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals).orEmpty() val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty()
return state.copyWrapped( return state.copyWrapped(
isEditState = isEditState, isEditState = isEditState,
amountState = amountState.copy( amountState = amountState.copy(

View file

@ -26,12 +26,15 @@ 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.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.extensions.shareText
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen 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.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.state.SendUiStateType
import com.tangem.features.send.impl.presentation.utils.getFiatFormatted
@Composable @Composable
internal fun SendNavigationButtons( internal fun SendNavigationButtons(
@ -172,21 +175,27 @@ private fun SendingText(
} }
if (feeFiat != null && sendingFiat != null) { if (feeFiat != null && sendingFiat != null) {
val sendingValue = BigDecimalFormatter.formatFiatAmount( val sendingValue = getFiatFormatted(
fiatAmount = sendingFiat, value = sendingFiat,
fiatCurrencyCode = feeState.appCurrency.code, currencySymbol = feeState.appCurrency.symbol,
fiatCurrencySymbol = feeState.appCurrency.symbol, currencyCode = feeState.appCurrency.code,
) )
val feeValue = BigDecimalFormatter.formatFiatAmount( val feeValue = getFiatFormatted(
fiatAmount = feeFiat, value = feeState.fee?.amount?.value,
fiatCurrencyCode = feeState.appCurrency.code, currencySymbol = feeState.appCurrency.symbol,
fiatCurrencySymbol = feeState.appCurrency.symbol, currencyCode = feeState.appCurrency.code,
) )
val textResource = remember(sendingValue, feeValue) {
resourceReference(
id = R.string.send_summary_transaction_description,
formatArgs = wrappedList(sendingValue, feeValue),
)
}
Text( Text(
text = stringResource(id = R.string.send_summary_transaction_description, sendingValue, feeValue), text = textResource.resolveAnnotatedReference(),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = TangemTheme.typography.caption1, style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary, color = TangemTheme.colors.text.primary1,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(TangemTheme.dimens.spacing12), .padding(TangemTheme.dimens.spacing12),

View file

@ -33,7 +33,10 @@ import kotlinx.coroutines.flow.withIndex
@Composable @Composable
internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) { internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
BackHandler(onBack = uiState.clickIntents::onBackClick) val onBackClick = uiState.clickIntents::onBackClick.takeIf {
uiState.sendState?.isSending != true
} ?: {}
BackHandler(onBack = onBackClick)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()

View file

@ -15,6 +15,7 @@ internal fun LazyListScope.notifications(
notifications: ImmutableList<SendNotification>, notifications: ImmutableList<SendNotification>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
hasPaddingAbove: Boolean = false, hasPaddingAbove: Boolean = false,
isClickDisabled: Boolean = false,
) { ) {
itemsIndexed( itemsIndexed(
items = notifications, items = notifications,
@ -44,6 +45,7 @@ internal fun LazyListScope.notifications(
-> null -> null
is SendNotification.Error -> TangemTheme.colors.icon.warning is SendNotification.Error -> TangemTheme.colors.icon.warning
}, },
isEnabled = !isClickDisabled,
) )
}, },
) )

View file

@ -16,6 +16,7 @@ import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.components.rows.SelectorRowItem
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.fee.FeeType
@ -114,11 +115,14 @@ private fun FeeError(feeSelectorState: FeeSelectorState) {
private fun FeeSelectorState.Content.getAmount(feeType: FeeType): Amount? { private fun FeeSelectorState.Content.getAmount(feeType: FeeType): Amount? {
val choosableFees = fees as? TransactionFee.Choosable val choosableFees = fees as? TransactionFee.Choosable
val decimals = fees.normal.amount.decimals
val customValue = this.customValues.firstOrNull()?.value?.parseToBigDecimal(decimals)
val customAmount = fees.normal.amount.copy(value = customValue)
return when (feeType) { return when (feeType) {
FeeType.Slow -> choosableFees?.minimum?.amount FeeType.Slow -> choosableFees?.minimum?.amount
FeeType.Market -> fees.normal.amount FeeType.Market -> fees.normal.amount
FeeType.Fast -> choosableFees?.priority?.amount FeeType.Fast -> choosableFees?.priority?.amount
FeeType.Custom -> null FeeType.Custom -> customAmount
} }
} }

View file

@ -33,9 +33,10 @@ internal fun TextFieldWithPaste(
) { ) {
val (title, color) = when { val (title, color) = when {
isError && error != null -> error to TangemTheme.colors.text.warning isError && error != null -> error to TangemTheme.colors.text.warning
isReadOnly -> label to TangemTheme.colors.text.disabled isReadOnly -> label to TangemTheme.colors.text.tertiary
else -> label to TangemTheme.colors.text.secondary else -> label to TangemTheme.colors.text.secondary
} }
val placeholderColor = if (isReadOnly) TangemTheme.colors.text.tertiary else TangemTheme.colors.text.disabled
FooterContainer(modifier, footer) { FooterContainer(modifier, footer) {
Box( Box(
modifier = Modifier modifier = Modifier
@ -59,6 +60,7 @@ internal fun TextFieldWithPaste(
SimpleTextField( SimpleTextField(
value = value, value = value,
placeholder = placeholder, placeholder = placeholder,
placeholderColor = placeholderColor,
onValueChange = onValueChange, onValueChange = onValueChange,
readOnly = isReadOnly, readOnly = isReadOnly,
modifier = Modifier modifier = Modifier

View file

@ -24,7 +24,7 @@ import com.tangem.features.send.impl.presentation.state.previewdata.AmountStateP
@Composable @Composable
internal fun AmountBlock( internal fun AmountBlock(
amountState: SendStates.AmountState, amountState: SendStates.AmountState,
isSuccess: Boolean, isClickDisabled: Boolean,
isEditingDisabled: Boolean, isEditingDisabled: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
@ -53,7 +53,7 @@ internal fun AmountBlock(
modifier = Modifier modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium) .clip(TangemTheme.shapes.roundedCornersXMedium)
.background(backgroundColor) .background(backgroundColor)
.clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick) .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
.padding( .padding(
vertical = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing14,
horizontal = TangemTheme.dimens.spacing16, horizontal = TangemTheme.dimens.spacing16,
@ -91,7 +91,7 @@ private fun AmountBlockPreview_Light(
TangemTheme { TangemTheme {
AmountBlock( AmountBlock(
amountState = value, amountState = value,
isSuccess = false, isClickDisabled = false,
isEditingDisabled = false, isEditingDisabled = false,
onClick = {}, onClick = {},
) )
@ -106,7 +106,7 @@ private fun AmountBlockPreview_Dark(
TangemTheme(isDark = true) { TangemTheme(isDark = true) {
AmountBlock( AmountBlock(
amountState = value, amountState = value,
isSuccess = true, isClickDisabled = true,
isEditingDisabled = false, isEditingDisabled = false,
onClick = {}, onClick = {},
) )

View file

@ -26,13 +26,13 @@ import com.tangem.features.send.impl.presentation.utils.getCryptoReference
import com.tangem.features.send.impl.presentation.utils.getFiatReference import com.tangem.features.send.impl.presentation.utils.getFiatReference
@Composable @Composable
internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) { internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, onClick: () -> Unit) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium) .clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action) .background(TangemTheme.colors.background.action)
.clickable(enabled = !isSuccess, onClick = onClick) .clickable(enabled = !isClickDisabled, onClick = onClick)
.padding(TangemTheme.dimens.spacing12), .padding(TangemTheme.dimens.spacing12),
) { ) {
Text( Text(
@ -115,7 +115,7 @@ private fun FeeBlockPreview_Light(@PreviewParameter(FeeBlockPreviewProvider::cla
TangemTheme { TangemTheme {
FeeBlock( FeeBlock(
feeState = value, feeState = value,
isSuccess = true, isClickDisabled = true,
onClick = {}, onClick = {},
) )
} }
@ -127,7 +127,7 @@ private fun FeeBlockPreview_Dark(@PreviewParameter(FeeBlockPreviewProvider::clas
TangemTheme(isDark = true) { TangemTheme(isDark = true) {
FeeBlock( FeeBlock(
feeState = value, feeState = value,
isSuccess = true, isClickDisabled = true,
onClick = {}, onClick = {},
) )
} }

View file

@ -23,7 +23,7 @@ import com.tangem.features.send.impl.presentation.state.previewdata.RecipientSta
@Composable @Composable
internal fun RecipientBlock( internal fun RecipientBlock(
recipientState: SendStates.RecipientState, recipientState: SendStates.RecipientState,
isSuccess: Boolean, isClickDisabled: Boolean,
isEditingDisabled: Boolean, isEditingDisabled: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
@ -38,7 +38,7 @@ internal fun RecipientBlock(
.fillMaxWidth() .fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium) .clip(TangemTheme.shapes.roundedCornersXMedium)
.background(backgroundColor) .background(backgroundColor)
.clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick) .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
.padding(TangemTheme.dimens.spacing12), .padding(TangemTheme.dimens.spacing12),
) { ) {
AddressBlock(recipientState.addressTextField) AddressBlock(recipientState.addressTextField)
@ -104,7 +104,7 @@ private fun RecipientBlockPreview_Light(
TangemTheme { TangemTheme {
RecipientBlock( RecipientBlock(
recipientState = value, recipientState = value,
isSuccess = true, isClickDisabled = true,
isEditingDisabled = false, isEditingDisabled = false,
onClick = {}, onClick = {},
) )
@ -119,7 +119,7 @@ private fun RecipientBlockPreview_Dark(
TangemTheme(isDark = true) { TangemTheme(isDark = true) {
RecipientBlock( RecipientBlock(
recipientState = value, recipientState = value,
isSuccess = true, isClickDisabled = true,
isEditingDisabled = false, isEditingDisabled = false,
onClick = {}, onClick = {},
) )

View file

@ -35,12 +35,13 @@ private const val TAP_HELP_ANIMATION_DELAY = 500L
@Composable @Composable
internal fun SendContent(uiState: SendUiState) { internal fun SendContent(uiState: SendUiState) {
val sendState = uiState.sendState ?: return val sendState = uiState.sendState ?: return
val isClickDisabled = sendState.isSending || sendState.isSuccess
LazyColumn( LazyColumn(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) { ) {
blocks(uiState) blocks(uiState)
tapHelp(isDisplay = sendState.showTapHelp) tapHelp(isDisplay = sendState.showTapHelp)
notifications(sendState.notifications) notifications(notifications = sendState.notifications, isClickDisabled = isClickDisabled)
} }
} }
@ -50,6 +51,7 @@ private fun LazyListScope.blocks(uiState: SendUiState) {
val feeState = uiState.feeState ?: return val feeState = uiState.feeState ?: return
val sendState = uiState.sendState ?: return val sendState = uiState.sendState ?: return
val isSuccess = sendState.isSuccess val isSuccess = sendState.isSuccess
val isClickDisabled = sendState.isSending || isSuccess
val timestamp = sendState.transactionDate val timestamp = sendState.transactionDate
item(key = BLOCKS_KEY) { item(key = BLOCKS_KEY) {
@ -65,19 +67,19 @@ private fun LazyListScope.blocks(uiState: SendUiState) {
} }
RecipientBlock( RecipientBlock(
recipientState = recipientState, recipientState = recipientState,
isSuccess = isSuccess, isClickDisabled = isClickDisabled,
isEditingDisabled = uiState.isEditingDisabled, isEditingDisabled = uiState.isEditingDisabled,
onClick = uiState.clickIntents::showRecipient, onClick = uiState.clickIntents::showRecipient,
) )
AmountBlock( AmountBlock(
amountState = amountState, amountState = amountState,
isSuccess = isSuccess, isClickDisabled = isClickDisabled,
isEditingDisabled = uiState.isEditingDisabled, isEditingDisabled = uiState.isEditingDisabled,
onClick = uiState.clickIntents::showAmount, onClick = uiState.clickIntents::showAmount,
) )
FeeBlock( FeeBlock(
feeState = feeState, feeState = feeState,
isSuccess = isSuccess, isClickDisabled = isClickDisabled,
onClick = uiState.clickIntents::showFee, onClick = uiState.clickIntents::showFee,
) )
} }

View file

@ -11,6 +11,7 @@ import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
private const val FIAT_DECIMALS = 2 private const val FIAT_DECIMALS = 2
private const val CRYPTO_FEE_DECIMALS = 6
private const val FEE_MINIMUM_VALUE = 0.01 private const val FEE_MINIMUM_VALUE = 0.01
internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? {
@ -21,7 +22,7 @@ internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): Tex
BigDecimalFormatter.formatCryptoAmount( BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = amount.value, cryptoAmount = amount.value,
cryptoCurrency = amount.currencySymbol, cryptoCurrency = amount.currencySymbol,
decimals = amount.decimals, decimals = amount.decimals.coerceAtMost(CRYPTO_FEE_DECIMALS),
), ),
), ),
) )
@ -36,24 +37,27 @@ internal fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency
internal fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String { internal fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String {
if (value == null || rate == null) return EMPTY_BALANCE_SIGN if (value == null || rate == null) return EMPTY_BALANCE_SIGN
val feeValue = value.multiply(rate) val feeValue = value.multiply(rate)
val scaled = feeValue.setScale(FIAT_DECIMALS, RoundingMode.UP) ?: BigDecimal.ZERO return getFiatFormatted(feeValue, appCurrency.code, appCurrency.symbol)
val formattedValue = if (scaled < BigDecimal(FEE_MINIMUM_VALUE)) { }
internal fun getFiatFormatted(value: BigDecimal?, currencyCode: String, currencySymbol: String): String {
val scaled = value?.setScale(FIAT_DECIMALS, RoundingMode.UP) ?: BigDecimal.ZERO
return if (scaled < BigDecimal(FEE_MINIMUM_VALUE)) {
buildString { buildString {
append(BigDecimalFormatter.CAN_BE_LOWER_SIGN) append(BigDecimalFormatter.CAN_BE_LOWER_SIGN)
append( append(
BigDecimalFormatter.formatFiatAmount( BigDecimalFormatter.formatFiatAmount(
fiatAmount = BigDecimal(FEE_MINIMUM_VALUE), fiatAmount = BigDecimal(FEE_MINIMUM_VALUE),
fiatCurrencyCode = appCurrency.code, fiatCurrencyCode = currencyCode,
fiatCurrencySymbol = appCurrency.symbol, fiatCurrencySymbol = currencySymbol,
), ),
) )
} }
} else { } else {
BigDecimalFormatter.formatFiatAmount( BigDecimalFormatter.formatFiatAmount(
fiatAmount = feeValue, fiatAmount = value,
fiatCurrencyCode = appCurrency.code, fiatCurrencyCode = currencyCode,
fiatCurrencySymbol = appCurrency.symbol, fiatCurrencySymbol = currencySymbol,
) )
} }
return formattedValue
} }

View file

@ -26,7 +26,6 @@ import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.tokens.utils.convertToAmount
import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.GetFeeError
@ -59,8 +58,9 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn import com.tangem.utils.coroutines.saveIn
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.* import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.math.BigDecimal import java.math.BigDecimal
import java.util.Locale import java.util.Locale
@ -79,8 +79,8 @@ internal class SendViewModel @Inject constructor(
private val getWalletsUseCase: GetWalletsUseCase, private val getWalletsUseCase: GetWalletsUseCase,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
private val getCryptoCurrencyStatusesSyncUseCase: GetCryptoCurrencyStatusesSyncUseCase, private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase,
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
private val getFeeUseCase: GetFeeUseCase, private val getFeeUseCase: GetFeeUseCase,
private val sendTransactionUseCase: SendTransactionUseCase, private val sendTransactionUseCase: SendTransactionUseCase,
@ -95,6 +95,7 @@ internal class SendViewModel @Inject constructor(
private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
currencyChecksRepository: CurrencyChecksRepository, currencyChecksRepository: CurrencyChecksRepository,
isFeeApproximateUseCase: IsFeeApproximateUseCase, isFeeApproximateUseCase: IsFeeApproximateUseCase,
validateWalletMemoUseCase: ValidateWalletMemoUseCase, validateWalletMemoUseCase: ValidateWalletMemoUseCase,
@ -180,6 +181,7 @@ internal class SendViewModel @Inject constructor(
stateRouterProvider = Provider { stateRouter }, stateRouterProvider = Provider { stateRouter },
currentStateProvider = Provider { uiState }, currentStateProvider = Provider { uiState },
analyticsEventHandler = analyticsEventHandler, analyticsEventHandler = analyticsEventHandler,
cryptoCurrencyProvider = Provider { cryptoCurrency },
) )
} }
@ -188,6 +190,7 @@ internal class SendViewModel @Inject constructor(
private set private set
private var userWallet: UserWallet by Delegates.notNull() private var userWallet: UserWallet by Delegates.notNull()
private var userWallets: List<AvailableWallet> = emptyList()
private var isAmountSubtractAvailable: Boolean = false private var isAmountSubtractAvailable: Boolean = false
private var isTapHelpPreviewEnabled: Boolean = false private var isTapHelpPreviewEnabled: Boolean = false
private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
@ -205,7 +208,6 @@ internal class SendViewModel @Inject constructor(
private var sendIdleTimer = 0L private var sendIdleTimer = 0L
init { init {
subscribeOnQRScannerResult()
subscribeOnCurrencyStatusUpdates() subscribeOnCurrencyStatusUpdates()
subscribeOnBalanceHidden() subscribeOnBalanceHidden()
getTapHelpPreviewAvailability() getTapHelpPreviewAvailability()
@ -372,7 +374,7 @@ internal class SendViewModel @Inject constructor(
cryptoCurrencyStatus = currencyStatus cryptoCurrencyStatus = currencyStatus
coinCryptoCurrencyStatus = coinCurrencyStatus coinCryptoCurrencyStatus = coinCurrencyStatus
feeCryptoCurrencyStatus = feeCurrencyStatus feeCryptoCurrencyStatus = feeCurrencyStatus
subscribeOnQRScannerResult()
when { when {
uiState.sendState?.isSuccess == true -> { uiState.sendState?.isSuccess == true -> {
stateRouter.showSend() stateRouter.showSend()
@ -400,57 +402,48 @@ internal class SendViewModel @Inject constructor(
} }
private fun getUserWallets() { private fun getUserWallets() {
getWalletsUseCase() viewModelScope.launch(dispatchers.main) {
.conflate() runCatching {
.distinctUntilChanged() getWalletsUseCase.invokeSync()
.onEach { userWallets -> ?.toAvailableWallets()
coroutineScope { .orEmpty()
runCatching { }.onSuccess { result ->
userWallets combine(*result.toTypedArray()) { it }
.filterNot { it.walletId == userWalletId || it.isLocked } .onEach {
.map { wallet -> userWallets = it.filterNotNull().toList()
async(dispatchers.io) { wallet.toAvailableWallet() } uiState = stateFactory.onLoadedWalletsList(wallets = userWallets)
}.awaitAll()
}.onSuccess { result ->
uiState = stateFactory.onLoadedWalletsList(wallets = result)
}.onFailure {
uiState = stateFactory.onLoadedWalletsList(wallets = emptyList())
} }
} .flowOn(dispatchers.main)
} .launchIn(viewModelScope)
.flowOn(dispatchers.main) }.onFailure {
.launchIn(viewModelScope) uiState = stateFactory.onLoadedWalletsList(wallets = emptyList())
}
private suspend fun UserWallet.toAvailableWallet(): AvailableWallet? {
return if (!isMultiCurrency) {
val status = getCryptoCurrencyStatusSyncUseCase(walletId).getOrNull()
val address = status?.value?.networkAddress.takeIf {
status?.currency?.network?.id == cryptoCurrency.network.id &&
status.currency.network.derivationPath !is Network.DerivationPath.Custom
}
address?.let {
AvailableWallet(
name = name,
address = it.defaultAddress.value,
)
}
} else {
val statuses = getCryptoCurrencyStatusesSyncUseCase(walletId).getOrNull()
val walletCurrency = statuses?.firstOrNull {
it.currency.network.id == cryptoCurrency.network.id &&
it.currency.network.derivationPath !is Network.DerivationPath.Custom
}
val address = walletCurrency?.value?.networkAddress
address?.let {
AvailableWallet(
name = name,
address = it.defaultAddress.value,
)
} }
} }
} }
private suspend fun List<UserWallet>.toAvailableWallets(): List<Flow<AvailableWallet?>> =
filterNot { it.walletId == userWalletId || it.isLocked }
.mapNotNull { wallet ->
val status = if (!wallet.isMultiCurrency) {
getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let {
if (it.network.id == cryptoCurrency.network.id) {
getNetworkAddressesUseCase(wallet.walletId, it.network)
} else {
null
}
}
} else {
getNetworkAddressesUseCase(wallet.walletId, cryptoCurrency.network)
}
status?.map { address ->
AvailableWallet(
name = wallet.name,
address = address,
userWalletId = wallet.walletId,
)
}
}
private suspend fun getTxHistory() { private suspend fun getTxHistory() {
val txHistoryList = getFixedTxHistoryItemsUseCase.getSync( val txHistoryList = getFixedTxHistoryItemsUseCase.getSync(
userWalletId = userWalletId, userWalletId = userWalletId,
@ -637,7 +630,7 @@ internal class SendViewModel @Inject constructor(
}.saveIn(memoValidationJobHolder) }.saveIn(memoValidationJobHolder)
} }
private suspend fun validateAddress(value: String): Boolean { private suspend fun validateAddress(value: String): Boolean = runCatching {
val isValidAddress = validateWalletAddressUseCase( val isValidAddress = validateWalletAddressUseCase(
userWalletId = userWalletId, userWalletId = userWalletId,
network = cryptoCurrency.network, network = cryptoCurrency.network,
@ -647,7 +640,7 @@ internal class SendViewModel @Inject constructor(
?.any { it.value == value } ?: true ?.any { it.value == value } ?: true
onEnteredValidAddress(isValidAddress, isAddressInWallet) onEnteredValidAddress(isValidAddress, isAddressInWallet)
return isValidAddress return isValidAddress
} }.getOrElse { false }
private suspend fun checkIfXrpAddressValue(value: String): Boolean { private suspend fun checkIfXrpAddressValue(value: String): Boolean {
return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress -> return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress ->
@ -875,11 +868,25 @@ internal class SendViewModel @Inject constructor(
uiState = stateFactory.getSendingStateUpdate(isSending = false) uiState = stateFactory.getSendingStateUpdate(isSending = false)
updateTransactionStatus(txData) updateTransactionStatus(txData)
scheduleBalanceUpdate() scheduleBalanceUpdate()
analyticsEventHandler.send(SendAnalyticEvents.TransactionScreenOpened) addTokenToWalletIfNeeded()
sendScreenAnalyticSender.sendTransaction()
}, },
) )
} }
private fun addTokenToWalletIfNeeded() {
if (cryptoCurrency !is CryptoCurrency.Token) return
val recipientState = uiState.getRecipientState(stateRouter.isEditState) ?: return
val destinationAddress = recipientState.addressTextField.value
val maybeUserWallet = userWallets.firstOrNull { it.address == destinationAddress } ?: return
viewModelScope.launch(dispatchers.io) {
addCryptoCurrenciesUseCase(userWalletId = maybeUserWallet.userWalletId, currency = cryptoCurrency)
}
}
private suspend fun updateTransactionStatus(txData: TransactionData) { private suspend fun updateTransactionStatus(txData: TransactionData) {
val txUrl = getExplorerTransactionUrlUseCase( val txUrl = getExplorerTransactionUrlUseCase(
userWalletId = userWalletId, userWalletId = userWalletId,

View file

@ -218,6 +218,7 @@ internal class StateBuilder(
quoteModel = quoteModel, quoteModel = quoteModel,
fromToken = fromToken, fromToken = fromToken,
ignoreAmountReduce = uiStateHolder.reduceAmountIgnore, ignoreAmountReduce = uiStateHolder.reduceAmountIgnore,
selectedFeeType = selectedFeeType,
) )
val feeState = createFeeState(quoteModel.txFee, selectedFeeType) val feeState = createFeeState(quoteModel.txFee, selectedFeeType)
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
@ -317,12 +318,13 @@ internal class StateBuilder(
quoteModel: SwapState.QuotesLoadedState, quoteModel: SwapState.QuotesLoadedState,
fromToken: CryptoCurrency, fromToken: CryptoCurrency,
ignoreAmountReduce: Boolean, ignoreAmountReduce: Boolean,
selectedFeeType: FeeType,
): List<SwapWarning> { ): List<SwapWarning> {
val warnings = mutableListOf<SwapWarning>() val warnings = mutableListOf<SwapWarning>()
maybeAddDomainWarnings(quoteModel, warnings, ignoreAmountReduce) maybeAddDomainWarnings(quoteModel, warnings, ignoreAmountReduce)
maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings) maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings)
maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken) maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken)
maybeAddNetworkFeeCoverageWarning(quoteModel, warnings) maybeAddNetworkFeeCoverageWarning(quoteModel, warnings, selectedFeeType)
maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings) maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings)
maybeAddInsufficientFundsWarning(quoteModel, warnings) maybeAddInsufficientFundsWarning(quoteModel, warnings)
maybeAddTransactionInProgressWarning(quoteModel, warnings) maybeAddTransactionInProgressWarning(quoteModel, warnings)
@ -461,18 +463,37 @@ internal class StateBuilder(
private fun maybeAddNetworkFeeCoverageWarning( private fun maybeAddNetworkFeeCoverageWarning(
quoteModel: SwapState.QuotesLoadedState, quoteModel: SwapState.QuotesLoadedState,
warnings: MutableList<SwapWarning>, warnings: MutableList<SwapWarning>,
selectedFeeType: FeeType,
) { ) {
when (quoteModel.preparedSwapConfigState.includeFeeInAmount) { when (quoteModel.preparedSwapConfigState.includeFeeInAmount) {
is IncludeFeeInAmount.Included -> is IncludeFeeInAmount.Included -> {
val fee = selectFeeByType(selectedFeeType, quoteModel.txFee) ?: return
warnings.add( warnings.add(
SwapWarning.GeneralWarning( SwapWarning.GeneralWarning(
createNetworkFeeCoverageNotificationConfig(), createNetworkFeeCoverageNotificationConfig(
quoteModel.fromTokenInfo.tokenAmount.getFormattedCryptoAmount(
quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency,
),
fee.feeFiatFormatted,
),
), ),
) )
}
else -> Unit else -> Unit
} }
} }
private fun selectFeeByType(feeType: FeeType, txFeeState: TxFeeState): TxFee? {
return when (txFeeState) {
TxFeeState.Empty -> null
is TxFeeState.SingleFeeState -> txFeeState.fee
is TxFeeState.MultipleFeeState -> when (feeType) {
FeeType.NORMAL -> txFeeState.normalFee
FeeType.PRIORITY -> txFeeState.priorityFee
}
}
}
private fun maybeAddUnableCoverFeeWarning( private fun maybeAddUnableCoverFeeWarning(
quoteModel: SwapState.QuotesLoadedState, quoteModel: SwapState.QuotesLoadedState,
fromToken: CryptoCurrency, fromToken: CryptoCurrency,
@ -548,12 +569,12 @@ internal class StateBuilder(
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
val warnings = mutableListOf<SwapWarning>() val warnings = mutableListOf<SwapWarning>()
warnings.add(getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency)) warnings.add(getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency))
if (includeFeeInAmount is IncludeFeeInAmount.Included) { if (includeFeeInAmount is IncludeFeeInAmount.Included && uiStateHolder.fee is FeeItemState.Content) {
warnings.add( val feeCoverageNotification = createNetworkFeeCoverageNotificationConfig(
SwapWarning.GeneralWarning( fromToken.tokenAmount.getFormattedCryptoAmount(fromToken.cryptoCurrencyStatus.currency),
createNetworkFeeCoverageNotificationConfig(), uiStateHolder.fee.amountFiatFormatted,
),
) )
warnings.add(SwapWarning.GeneralWarning(feeCoverageNotification))
} }
val providerState = getProviderStateForError( val providerState = getProviderStateForError(
swapProvider = swapProvider, swapProvider = swapProvider,
@ -1397,10 +1418,16 @@ internal class StateBuilder(
) )
} }
private fun createNetworkFeeCoverageNotificationConfig(): NotificationConfig { private fun createNetworkFeeCoverageNotificationConfig(
cryptoAmount: String,
fiatAmount: String,
): NotificationConfig {
return NotificationConfig( return NotificationConfig(
title = resourceReference(R.string.send_network_fee_warning_title), title = resourceReference(R.string.send_network_fee_warning_title),
subtitle = resourceReference(R.string.swapping_network_fee_warning_content), subtitle = resourceReference(
R.string.common_network_fee_warning_content,
wrappedList(cryptoAmount, fiatAmount),
),
iconResId = R.drawable.img_attention_20, iconResId = R.drawable.img_attention_20,
) )
} }

View file

@ -42,6 +42,7 @@ internal class TokenDetailsNotificationsAnalyticsSender(
is TokenDetailsNotification.TopUpWithoutReserve, is TokenDetailsNotification.TopUpWithoutReserve,
is TokenDetailsNotification.RentInfo, is TokenDetailsNotification.RentInfo,
is TokenDetailsNotification.SwapPromo, is TokenDetailsNotification.SwapPromo,
is TokenDetailsNotification.NetworkShutdown,
-> null -> null
} }
} }

View file

@ -155,7 +155,11 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
), ),
) )
class NetworksNoAccount(val network: String, val symbol: String, val amount: String) : Informational( data class NetworksNoAccount(
private val network: String,
private val symbol: String,
private val amount: String,
) : Informational(
title = resourceReference(R.string.warning_no_account_title), title = resourceReference(R.string.warning_no_account_title),
subtitle = resourceReference( subtitle = resourceReference(
id = R.string.no_account_generic, id = R.string.no_account_generic,
@ -175,4 +179,9 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
formatArgs = wrappedList(coinSymbol), formatArgs = wrappedList(coinSymbol),
), ),
) )
data class NetworkShutdown(private val title: TextReference, private val subtitle: TextReference) : Warning(
title = title,
subtitle = subtitle,
)
} }

View file

@ -1,6 +1,7 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -8,6 +9,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification.*
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.removeBy import com.tangem.utils.extensions.removeBy
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -78,6 +80,10 @@ internal class TokenDetailsNotificationConverter(
onSwapClick = clickIntents::onSwapPromoClick, onSwapClick = clickIntents::onSwapPromoClick,
onCloseClick = clickIntents::onSwapPromoDismiss, onCloseClick = clickIntents::onSwapPromoDismiss,
) )
is CryptoCurrencyWarning.BeaconChainShutdown -> NetworkShutdown(
title = resourceReference(R.string.warning_beacon_chain_retirement_title),
subtitle = resourceReference(R.string.warning_beacon_chain_retirement_content),
)
} }
} }

View file

@ -1,8 +1,12 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.UpdateWalletUseCase import com.tangem.domain.wallets.usecase.UpdateWalletUseCase
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
@ -32,9 +36,13 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
private val stateHolder: WalletStateController, private val stateHolder: WalletStateController,
private val walletEventSender: WalletEventSender, private val walletEventSender: WalletEventSender,
private val walletScreenContentLoader: WalletScreenContentLoader, private val walletScreenContentLoader: WalletScreenContentLoader,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val updateWalletUseCase: UpdateWalletUseCase, private val updateWalletUseCase: UpdateWalletUseCase,
private val deleteWalletUseCase: DeleteWalletUseCase, private val deleteWalletUseCase: DeleteWalletUseCase,
private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase,
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
private val reduxStateHolder: ReduxStateHolder,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
) : BaseWalletClickIntents(), WalletCardClickIntents { ) : BaseWalletClickIntents(), WalletCardClickIntents {
@ -72,7 +80,18 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) { override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) {
viewModelScope.launch(dispatchers.main) { viewModelScope.launch(dispatchers.main) {
walletScreenContentLoader.cancel(userWalletId) walletScreenContentLoader.cancel(userWalletId)
val deletedUserWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch
deleteSavedAccessCodesUseCase(cardId = deletedUserWallet.cardId)
.onLeft { Timber.e(it.toString()) }
deleteWalletUseCase(userWalletId) deleteWalletUseCase(userWalletId)
.onRight {
getSelectedWalletSyncUseCase().getOrNull()?.let {
reduxStateHolder.onUserWalletSelected(it)
}
}
.onLeft { Timber.e(it.toString()) } .onLeft { Timber.e(it.toString()) }
} }
} }

View file

@ -82,12 +82,13 @@ swipeRefreshLayout = "1.1.0"
spr-client = "3.6.2" spr-client = "3.6.2"
web3j = "4.10.1" web3j = "4.10.1"
leakcanary = "2.13" leakcanary = "2.13"
markdown = "0.7.2"
# endregion Other libraries # endregion Other libraries
# region Tangem # region Tangem
tangemBlockchainSdk = "release-app_5.10-618" tangemBlockchainSdk = "release-app_5.10-630"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "release-app_5.9-343" tangemCardSdk = "release-app_5.10-353"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
# endregion Tangem # endregion Tangem
@ -249,4 +250,5 @@ camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref =
camera-view = { module = "androidx.camera:camera-view", version.ref = "androidXCamera" } camera-view = { module = "androidx.camera:camera-view", version.ref = "androidXCamera" }
web3j-core = { module = "org.web3j:core", version.ref = "web3j" } web3j-core = { module = "org.web3j:core", version.ref = "web3j" }
leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" }
markdown = { module = "org.jetbrains:markdown", version.ref = "markdown" }
# endregion Other # endregion Other

View file

@ -44,4 +44,10 @@ object BlockchainUtils {
val blockchain = Blockchain.fromId(networkId) val blockchain = Blockchain.fromId(networkId)
return blockchain == Blockchain.Tezos return blockchain == Blockchain.Tezos
} }
/** If current [networkId] is BeaconChain */
fun isBeaconChain(networkId: String): Boolean {
val blockchain = Blockchain.fromId(networkId)
return blockchain == Blockchain.Binance || blockchain == Blockchain.BinanceTestnet
}
} }