Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-10 16:22:09 +07:00
commit 208e63c414
73 changed files with 547 additions and 151 deletions

View file

@ -71,7 +71,7 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
private fun storeConversionData(refcode: String, campaign: String?) {
coroutineScope.launch {
mutex.withLock {
setShouldShowMobileWalletPromoUseCase()
setShouldShowMobileWalletPromoUseCase(true)
.onLeft { Timber.e(it) }
appsFlyerStore.storeIfAbsent(
value = AppsFlyerConversionData(refcode = refcode, campaign = campaign),

View file

@ -2,12 +2,22 @@ package com.tangem.tap.core.security
import com.dexprotector.rtc.RtcStatus
import com.tangem.security.DeviceSecurityInfoProvider
import timber.log.Timber
internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider {
override val isRooted: Boolean
get() = RtcStatus.getRtcStatus().root
get() = getRtcStatusSafely()?.root == true
override val isBootloaderUnlocked: Boolean
get() = RtcStatus.getRtcStatus().unlockedBootloader
get() = getRtcStatusSafely()?.unlockedBootloader == true
override val isXposed: Boolean
get() = RtcStatus.getRtcStatus().xposed
get() = getRtcStatusSafely()?.xposed == true
private fun getRtcStatusSafely(): RtcStatus? {
return try {
RtcStatus.getRtcStatus()
} catch (e: Throwable) {
Timber.e(e)
null
}
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.tap.core.ui
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import javax.inject.Inject
class DefaultHoldToConfirmButtonFeatureToggles @Inject constructor(
featureTogglesManager: FeatureTogglesManager,
) : HoldToConfirmButtonFeatureToggles {
override val isHoldToConfirmEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
"HOLD_TO_CONFIRM_BUTTON_ENABLED",
)
}

View file

@ -1,7 +1,9 @@
package com.tangem.tap.di.core.ui
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.tap.core.ui.DefaultDesignFeatureToggles
import com.tangem.tap.core.ui.DefaultHoldToConfirmButtonFeatureToggles
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -13,4 +15,9 @@ interface CoreUiBindsModule {
@Binds
fun bindDesignFeatureToggles(impl: DefaultDesignFeatureToggles): DesignFeatureToggles
@Binds
fun bindHoldToConfirmButtonFeatureToggles(
impl: DefaultHoldToConfirmButtonFeatureToggles,
): HoldToConfirmButtonFeatureToggles
}

View file

@ -7,6 +7,7 @@ import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.settings.*
import com.tangem.domain.settings.repositories.AppRatingRepository
import com.tangem.domain.settings.repositories.LegacySettingsRepository
import com.tangem.domain.settings.repositories.PermissionRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
@ -68,10 +69,14 @@ internal object SettingsDomainModule {
@Provides
@Singleton
fun providesCanUseBiometryUseCase(tangemSdkManager: TangemSdkManager): CanUseBiometryUseCase {
return CanUseBiometryUseCase(
legacySettingsRepository = DefaultLegacySettingsRepository(tangemSdkManager = tangemSdkManager),
)
fun provideLegacySettingsRepository(tangemSdkManager: TangemSdkManager): LegacySettingsRepository {
return DefaultLegacySettingsRepository(tangemSdkManager = tangemSdkManager)
}
@Provides
@Singleton
fun providesCanUseBiometryUseCase(legacySettingsRepository: LegacySettingsRepository): CanUseBiometryUseCase {
return CanUseBiometryUseCase(legacySettingsRepository = legacySettingsRepository)
}
@Provides

View file

@ -438,6 +438,7 @@ internal class DefaultTangemSdkManager(
),
)
showAlert()
break
} else {
delay(timeMillis = 400)
}

View file

@ -8,4 +8,8 @@ internal class DefaultLegacySettingsRepository(
) : LegacySettingsRepository {
override suspend fun canUseBiometry(): Boolean = tangemSdkManager.checkCanUseBiometry()
override suspend fun canUseBiometryStrict(): Boolean {
return tangemSdkManager.checkCanUseBiometry() && tangemSdkManager.checkNeedEnrollBiometrics().not()
}
}

View file

@ -16,6 +16,7 @@ import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.feature.referral.domain.MobileWalletPromoRepository
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.AndroidSecureStorageV2
@ -54,6 +55,7 @@ internal object UserWalletsListManagerModule {
trackingContextProxy: TrackingContextProxy,
analyticsEventHandler: AnalyticsEventHandler,
hotWalletRepository: HotWalletRepository,
mobileWalletPromoRepository: MobileWalletPromoRepository,
): UserWalletsListRepository {
val moshi = buildMoshi()
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
@ -104,6 +106,7 @@ internal object UserWalletsListManagerModule {
trackingContextProxy = trackingContextProxy,
analyticsEventHandler = analyticsEventHandler,
hotWalletRepository = hotWalletRepository,
mobileWalletPromoRepository = mobileWalletPromoRepository,
)
}

View file

@ -26,6 +26,7 @@ import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.feature.referral.domain.MobileWalletPromoRepository
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.sdk.api.TangemSdkManager
@ -60,6 +61,7 @@ internal class DefaultUserWalletsListRepository(
private val trackingContextProxy: TrackingContextProxy,
private val analyticsEventHandler: AnalyticsEventHandler,
private val hotWalletRepository: HotWalletRepository,
private val mobileWalletPromoRepository: MobileWalletPromoRepository,
) : UserWalletsListRepository {
override val userWallets = MutableStateFlow<List<UserWallet>?>(null)
@ -144,6 +146,8 @@ internal class DefaultUserWalletsListRepository(
raise(SaveWalletError.WalletAlreadySaved(messageId = R.string.user_wallet_list_error_wallet_already_saved))
}
val isFirstWallet = userWallets.value?.isEmpty() == true
if (savePersistentInformation()) {
publicInformationRepository.save(userWallet, canOverride)
if (userWallet.isLocked.not()) {
@ -166,6 +170,10 @@ internal class DefaultUserWalletsListRepository(
wallets.addOrReplace(userWallet) { it.walletId == userWallet.walletId }
}
if (isFirstWallet) {
onFirstWalletCreated()
}
// update the selectedUserWallet state if it is the only wallet
if (userWallets.value?.size == 1) {
selectedUserWalletRepository.set(userWallet.walletId)
@ -239,6 +247,9 @@ internal class DefaultUserWalletsListRepository(
val newSelected = updatedWallets?.findAvailableUserWallet(
currentWallets.indexOfFirstOrNull { it.walletId == currentSelected.walletId } ?: 0,
)
if (newSelected == null) {
onAllWalletsDeleted()
}
selectedUserWalletRepository.set(newSelected?.walletId)
newSelected
}
@ -595,4 +606,14 @@ internal class DefaultUserWalletsListRepository(
private fun trackWalletUpgradeEvent() {
analyticsEventHandler.send(event = WalletSettingsAnalyticEvents.WalletUpgraded())
}
private suspend fun onFirstWalletCreated() {
// reset flag (that is set from AF deeplink) after creating a new wallet
mobileWalletPromoRepository.setShouldShowMobileWalletPromo(false)
}
private suspend fun onAllWalletsDeleted() {
// reset flag (that is set from AF deeplink) after removing the last wallet
mobileWalletPromoRepository.setShouldShowMobileWalletPromo(false)
}
}

View file

@ -30,10 +30,14 @@ class RootDetectedWarningComponent @AssistedInject constructor(
private val isShown = instanceKeeper.getOrCreateSimple { MutableStateFlow(false) }
suspend fun shouldShowWarning(): Boolean {
return settingsRepository.isRootDetectedWarningShown().not() && securityInfoProvider.isSecurityExposed()
}
suspend fun tryToShowWarningAndWaitContinuation() {
if (isShown.value) return
if (settingsRepository.isRootDetectedWarningShown().not() && securityInfoProvider.isSecurityExposed()) {
if (shouldShowWarning()) {
isShown.value = true
}

View file

@ -139,9 +139,15 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private fun initializeInitialNavigation() {
if (initialStack.isNullOrEmpty()) {
componentScope.launch {
rootDetectedWarningComponent.tryToShowWarningAndWaitContinuation()
val initialRoute = resolveInitialRoute()
router.replaceAll(initialRoute)
if (rootDetectedWarningComponent.shouldShowWarning()) {
launch(dispatchers.main) {
rootDetectedWarningComponent.tryToShowWarningAndWaitContinuation()
router.replaceAll(initialRoute)
}
} else {
router.replaceAll(initialRoute)
}
}
}
}

View file

@ -26,7 +26,7 @@ class AppsFlyerReferralParamsHandlerTest {
private val appsFlyerStore: AppsFlyerStore = mockk(relaxUnitFun = true)
private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase = mockk {
coEvery { this@mockk.invoke() } returns Unit.right()
coEvery { this@mockk.invoke(true) } returns Unit.right()
}
private val handler = AppsFlyerReferralParamsHandler(
appsFlyerStore = appsFlyerStore,

View file

@ -75,6 +75,10 @@
"name": "EARN_BLOCK_ENABLED",
"version": "undefined"
},
{
"name": "HOLD_TO_CONFIRM_BUTTON_ENABLED",
"version": "undefined"
},
{
"name": "WALLET_REORDER_FEATURE_ENABLED",
"version": "undefined"

View file

@ -49,6 +49,7 @@ internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel,
polygonScanApiKey = value.polygonScanApiKey,
bittensorDwellirApiKey = value.bittensorDwellirApiKey,
bittensorOnfinalityApiKey = value.bittensorOnfinalityKey,
dwellirApiKey = value.dwellirApiKey,
koinosProApiKey = value.koinosProApiKey,
alephiumApiKey = value.alephiumTangemApiKey,
moralisApiKey = value.moralisApiKey,

View file

@ -40,6 +40,7 @@ class EnvironmentConfigModel(
@Json(name = "p2pApiKey") val p2pApiKey: P2PKeys?,
@Json(name = "bittensorDwellirKey") val bittensorDwellirApiKey: String?,
@Json(name = "bittensorOnfinalityKey") val bittensorOnfinalityKey: String?,
@Json(name = "dwellirApiKey") val dwellirApiKey: String?,
@Json(name = "koinosProApiKey") val koinosProApiKey: String?,
@Json(name = "alephiumTangemApiKey") val alephiumTangemApiKey: String?,
@Json(name = "moralisApiKey") val moralisApiKey: String?,

View file

@ -1484,10 +1484,12 @@
<string name="tangem_pay_status_completed">Abgeschlossen</string>
<string name="tangem_pay_status_declined">Abgelehnt</string>
<string name="tangem_pay_status_pending">Ausstehend</string>
<string name="tangem_pay_status_reversed">Storniert</string>
<string name="tangem_pay_terms_fees_limits">Bedingungen, Gebühren &amp; Limits</string>
<string name="tangem_pay_terms_limits">Bedingungen und Einschränkungen</string>
<string name="tangem_pay_transaction_declined_notification_text">Die Bank hat diese Transaktionsanfrage abgelehnt.</string>
<string name="tangem_pay_transaction_fee_notification_text">Diese Gebühr dient zur Deckung der Kosten für die Abwicklung Deiner Überweisung.Questa commissione copre il costo della gestione del tuo trasferimento.</string>
<string name="tangem_pay_transaction_fee_notification_text">Diese Gebühr dient zur Deckung der Kosten für die Abwicklung Deiner Überweisung.</string>
<string name="tangem_pay_transaction_reversed_notification_text">Die Transaktion wurde vom Händler teilweise oder vollständig storniert</string>
<string name="tangem_pay_unfreeze_card_alert_body">Nutze Dein Geld weiterhin. Du kannst es jederzeit einfrieren.</string>
<string name="tangem_pay_unfreeze_card_alert_title">Karte entsperren?</string>
<string name="tangem_pay_unfreeze_card_failed">Entsperren der Karte fehlgeschlagen. Versuchen Sie es später erneut.</string>

View file

@ -1481,10 +1481,12 @@
<string name="tangem_pay_status_completed">Completado</string>
<string name="tangem_pay_status_declined">Rechazado</string>
<string name="tangem_pay_status_pending">Pendiente</string>
<string name="tangem_pay_status_reversed">Revertida</string>
<string name="tangem_pay_terms_fees_limits">Términos, tarifas y límites</string>
<string name="tangem_pay_terms_limits">Términos y límites</string>
<string name="tangem_pay_transaction_declined_notification_text">El banco rechazó esta solicitud de transacción.</string>
<string name="tangem_pay_transaction_fee_notification_text">Esta tarifa cubre el costo de procesar tu transferencia.</string>
<string name="tangem_pay_transaction_reversed_notification_text">La transacción fue revertida parcial o totalmente por el comerciante</string>
<string name="tangem_pay_unfreeze_card_alert_body">Sigue usando tu dinero. Puedes congelarlo en cualquier momento.</string>
<string name="tangem_pay_unfreeze_card_alert_title">¿Descongelar tu tarjeta?</string>
<string name="tangem_pay_unfreeze_card_failed">No se pudo descongelar la tarjeta. Inténtalo de nuevo más tarde.</string>

View file

@ -1487,10 +1487,12 @@
<string name="tangem_pay_status_completed">Terminé</string>
<string name="tangem_pay_status_declined">Refusé</string>
<string name="tangem_pay_status_pending">En attente</string>
<string name="tangem_pay_status_reversed">Annulée</string>
<string name="tangem_pay_terms_fees_limits">Conditions, frais et limites</string>
<string name="tangem_pay_terms_limits">Conditions et limites</string>
<string name="tangem_pay_transaction_declined_notification_text">La banque a rejeté cette demande de transaction.</string>
<string name="tangem_pay_transaction_fee_notification_text">Ces frais couvrent le coût du traitement de votre virement.</string>
<string name="tangem_pay_transaction_reversed_notification_text">La transaction a été partiellement ou totalement annulée par le commerçant</string>
<string name="tangem_pay_unfreeze_card_alert_body">Continuez à utiliser votre argent. Vous pouvez le geler à tout moment.</string>
<string name="tangem_pay_unfreeze_card_alert_title">Dégeler votre carte ?</string>
<string name="tangem_pay_unfreeze_card_failed">Échec du dégel de la carte. Réessayez plus tard.</string>

View file

@ -87,10 +87,12 @@
<string name="tangem_pay_status_completed">Completato</string>
<string name="tangem_pay_status_declined">Rifiutato</string>
<string name="tangem_pay_status_pending">In sospeso</string>
<string name="tangem_pay_status_reversed">Stornata</string>
<string name="tangem_pay_terms_fees_limits">Termini, commissioni e limiti</string>
<string name="tangem_pay_terms_limits">Termini e limiti</string>
<string name="tangem_pay_transaction_declined_notification_text">La banca ha rifiutato questa richiesta di transazione.</string>
<string name="tangem_pay_transaction_fee_notification_text">Questa commissione copre il costo della gestione del tuo trasferimento.</string>
<string name="tangem_pay_transaction_reversed_notification_text">La transazione è stata parzialmente o totalmente stornata dal commerciante</string>
<string name="tangem_pay_unfreeze_card_alert_body">Continua a usare i tuoi soldi. Puoi congelarli in qualsiasi momento.</string>
<string name="tangem_pay_unfreeze_card_alert_title">Sbloccare la tua carta?</string>
<string name="tangem_pay_unfreeze_card_failed">Impossibile sbloccare la carta. Riprova più tardi.</string>

View file

@ -86,7 +86,7 @@
<string name="address_qr_code_message_format">このアドレスには%3$s ネットワークから%1$s (%2$s) のみを送信してください。他のトークンやネットワークを使用すると、資金を失う可能性があります。</string>
<string name="address_type_default">デフォルト</string>
<string name="address_type_legacy">レガシー</string>
<string name="alert_authentication_error_message">デバイスの生体認証をリセットするか、サポートにお問い合わせください</string>
<string name="alert_authentication_error_message">生体認証で問題が発生しました。端末の生体認証をリセットするか、サポートにお問い合わせください</string>
<string name="alert_authentication_error_title">認証エラー</string>
<string name="alert_button_how_to_scan">スキャン方法</string>
<string name="alert_button_request_support">サポートをリクエストする</string>
@ -461,9 +461,15 @@
<string name="domain_receive_assets_onboarding_network_name">%sネットワーク</string>
<string name="domain_receive_assets_onboarding_title">下記のみを使用して資金を送金する</string>
<string name="earn_best_opportunities">おすすめ</string>
<string name="earn_clear_filter">絞り込みを解除</string>
<string name="earn_empty">リストは現在更新中のため、一時的に空になっています。しばらくしてからご確認ください。</string>
<string name="earn_filter_all_networks">すべてのネットワーク</string>
<string name="earn_filter_all_types">すべての種類</string>
<string name="earn_filter_by">絞り込み</string>
<string name="earn_filter_my_networks">マイネットワーク</string>
<string name="earn_filter_networks">ネットワーク</string>
<string name="earn_mostly_used">よく使われています</string>
<string name="earn_no_results">該当する結果はありません</string>
<string name="earn_title">運用</string>
<string name="email_preface_wc_error">こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。</string>
<string name="email_subject_wc_error">WalletConnectエラー</string>
@ -814,6 +820,7 @@
<string name="markets_token_details_volume">取引量</string>
<string name="markets_tooltip_message">これをドラッグするか、検索窓をタップして、マーケットから直接トークンを追加します</string>
<string name="markets_tooltip_title">トークンを追加</string>
<string name="markets_tooltip_v2_title">トークンを追加</string>
<string name="markets_yield_supply_banner_description">資産を常時アクセス可能な状態に保ったまま、パワーアップさせよう。%s</string>
<string name="markets_yield_supply_banner_title">利息モードを有効にする</string>
<string name="mobile_wallet_requires_min_os_warning_body">モバイルウォレットを作成するには、%1$sにアップデートする必要があります</string>
@ -827,7 +834,7 @@
<item quantity="other">%d分前</item>
</plurals>
<string name="news_quick_recap">クイックまとめ</string>
<string name="news_related_news">関連ニュース</string>
<string name="news_related_news">ニュース</string>
<string name="news_related_tokens">関連トークン</string>
<string name="news_sources">関連ニュース</string>
<string name="news_stay_in_the_loop">最新情報を入手</string>
@ -1462,10 +1469,12 @@
<string name="tangem_pay_status_completed">完了</string>
<string name="tangem_pay_status_declined">拒否</string>
<string name="tangem_pay_status_pending">保留中</string>
<string name="tangem_pay_status_reversed">取消済み</string>
<string name="tangem_pay_terms_fees_limits">利用規約・手数料・利用制限</string>
<string name="tangem_pay_terms_limits">利用規約と上限条件</string>
<string name="tangem_pay_transaction_declined_notification_text">銀行がこの取引リクエストを拒否しました。</string>
<string name="tangem_pay_transaction_fee_notification_text">この手数料は、送金処理にかかるコストをカバーするためのものです。</string>
<string name="tangem_pay_transaction_reversed_notification_text">この取引は加盟店により一部または全額取り消されました</string>
<string name="tangem_pay_unfreeze_card_alert_body">資金は引き続き使用できます。いつでも一時停止できます。</string>
<string name="tangem_pay_unfreeze_card_alert_title">カードの一時停止を解除しますか?</string>
<string name="tangem_pay_unfreeze_card_failed">カードの凍結解除に失敗しました。しばらくしてからもう一度お試しください。</string>

View file

@ -485,6 +485,7 @@
<string name="earn_filter_all_networks">Все сети</string>
<string name="earn_filter_all_types">Все типы</string>
<string name="earn_filter_by">Фильтровать по</string>
<string name="earn_filter_my_networks">Мои сети</string>
<string name="earn_filter_networks">Сети</string>
<string name="earn_mostly_used">Часто используемые</string>
<string name="earn_no_results">Нет результата</string>
@ -843,6 +844,7 @@
<string name="markets_token_details_volume">Объем</string>
<string name="markets_tooltip_message">Потяните вверх или коснитесь поисковой строки, чтобы добавить токены напрямую из рынка</string>
<string name="markets_tooltip_title">Добавить токены</string>
<string name="markets_tooltip_v2_title">Добавить токены</string>
<string name="markets_yield_supply_banner_description">Увеличивайте доход с активов, сохраняя мгновенный доступ к ним. %s</string>
<string name="markets_yield_supply_banner_title">Активировать режим доходности</string>
<string name="mobile_wallet_requires_min_os_warning_body">Обновитесь до версии %1$s, чтобы создать мобильный кошелёк</string>
@ -1514,10 +1516,12 @@
<string name="tangem_pay_status_completed">Успешно завершено</string>
<string name="tangem_pay_status_declined">Отклонено</string>
<string name="tangem_pay_status_pending">В процессе</string>
<string name="tangem_pay_status_reversed">Возврат</string>
<string name="tangem_pay_terms_fees_limits">Тарифы и полные условия</string>
<string name="tangem_pay_terms_limits">Тарифы и лимиты</string>
<string name="tangem_pay_transaction_declined_notification_text">Банк отклонил транзакцию</string>
<string name="tangem_pay_transaction_fee_notification_text">Эта комиссия покрывает стоимость обработки вашего перевода.</string>
<string name="tangem_pay_transaction_reversed_notification_text">Транзакция частично или полностью возвращена продавцом</string>
<string name="tangem_pay_unfreeze_card_alert_body">Продолжайте пользоваться картой, заморозить всегда успеете</string>
<string name="tangem_pay_unfreeze_card_alert_title">Разморозить карту?</string>
<string name="tangem_pay_unfreeze_card_failed">Не удалось разморозить карту, попробуйте еще раз</string>
@ -1928,6 +1932,7 @@
<string name="wc_alert_unknown_error_title">Мы обнаружили неизвестную ошибку</string>
<string name="wc_alert_unsupported_dapps_description">Кошелек Tangem в настоящий момент не поддерживает %s</string>
<string name="wc_alert_unsupported_dapps_title">Неподдерживаемый dApp</string>
<string name="wc_alert_unsupported_method_description">Код ошибки: 8 005.\nЕсли проблема сохраняется — пожалуйста, свяжитесь с нашей службой поддержки.</string>
<string name="wc_alert_unsupported_method_title">Мы обнаружили неизвестную ошибку</string>
<string name="wc_alert_unsupported_network_description">Эта сеть %s не поддерживается Tangem Wallet и не может быть подключена.</string>
<string name="wc_alert_unsupported_network_title">Неподдерживаемая сеть</string>

View file

@ -311,6 +311,7 @@
<string name="common_go_to_token">Перейти до токену</string>
<string name="common_got_it">Зrozуміло</string>
<string name="common_hide">Приховати</string>
<string name="common_hold_to">Утримуйте, щоб %s</string>
<string name="common_hour">година</string>
<string name="common_import">Імпортувати</string>
<string name="common_in_progress">В процесі</string>
@ -337,10 +338,12 @@
<string name="common_no">Ні</string>
<string name="common_no_address">Немає адреси</string>
<string name="common_not_added">Не додано</string>
<string name="common_not_available">Недоступно</string>
<string name="common_not_now">Не зараз</string>
<string name="common_now">Зараз</string>
<string name="common_ok">ОК</string>
<string name="common_open_in_browser">Відкрити в браузері</string>
<string name="common_or">або</string>
<string name="common_origin_card">Основна картка</string>
<string name="common_origin_ring">Основне кільце</string>
<string name="common_passphrase">Парольна фраза</string>
@ -386,6 +389,7 @@
<string name="common_swap">Обмін</string>
<string name="common_tangem">Tangem</string>
<string name="common_tangem_wallet">Tangem Wallet</string>
<string name="common_tap_and_hold_hint">Натисніть і утримуйте</string>
<string name="common_terms_and_conditions">умови участі</string>
<string name="common_terms_of_use">Умовами використання</string>
<string name="common_to">До</string>
@ -453,6 +457,7 @@
<string name="details_manage_security_long_tap_description">Цей механізм захищає картку або кільце від безконтактних атак. Між скануванням картки та виконанням команди буде додана затримка.</string>
<string name="details_manage_security_passcode">Пароль</string>
<string name="details_manage_security_passcode_description">Перед виконанням будь-якої команди, що тягне за собою зміну стану картки, вам необхідно буде ввести пароль.</string>
<string name="details_mobile_wallet_upgrade_action_title">Оновлення до апаратного гаманця</string>
<string name="details_nft_title">NFT</string>
<string name="details_referral_title">Реферальна програма</string>
<string name="details_row_description_flip_to_hide">Переверніть екран пристрою вниз, щоб швидко приховати та відобразити баланси</string>
@ -466,6 +471,7 @@
<string name="details_row_title_signed_hashes">Підписано</string>
<string name="details_send_feedback">Надіслати відгук</string>
<string name="details_title">Деталі</string>
<string name="details_wallets_section_description">Ви можете мати лише один мобільний гаманець. Оновіть його до апаратного гаманця Tangem або додайте новий апаратний гаманець.</string>
<string name="disclaimer_error_loading">Перевірте підключення до інтернету або змініть мережу</string>
<string name="disclaimer_title">Умови використання</string>
<string name="domain_receive_assets_default_address">Основна адреса</string>
@ -475,6 +481,15 @@
<string name="domain_receive_assets_onboarding_description">Надсилання активів в інші мережі призведе до безповоротної втрати.</string>
<string name="domain_receive_assets_onboarding_network_name">%s мережа</string>
<string name="domain_receive_assets_onboarding_title">Надсилайте кошти, використовуючи лише</string>
<string name="earn_best_opportunities">Найкращі можливості</string>
<string name="earn_clear_filter">Очистити фільтр</string>
<string name="earn_filter_all_networks">Всі мережі</string>
<string name="earn_filter_all_types">Всі типи</string>
<string name="earn_filter_by">Фільтрувати за</string>
<string name="earn_filter_my_networks">Мої мережі</string>
<string name="earn_filter_networks">Мережі</string>
<string name="earn_mostly_used">Часто використовані</string>
<string name="earn_no_results">Немає результатів</string>
<string name="email_preface_wc_error">Привіт, команда підтримки, я зіткнувся з помилкою з кодом: %s</string>
<string name="email_subject_wc_error">Помилка WalletConnect</string>
<string name="error_wrong_wallet_tapped">Ви використали картку або кільце від іншого гаманця. Прикладіть картку або кільце, пов\'язану з цим гаманцем.</string>
@ -555,6 +570,10 @@
<string name="express_transaction_id">ID: %s</string>
<string name="express_transaction_id_copied">ID транзакції скопійовано</string>
<string name="exсhange_token_description">Обміняйте будь-який актив у своєму портфелі на цей токен</string>
<string name="fee_selector_choose_speed_description">Більш вища швидкість означає більш швидше підтвердження, але і більш вищу мережеву комісію. %s</string>
<string name="fee_selector_choose_speed_title">Обрати швидкість</string>
<string name="fee_selector_choose_token_description">Оберіть, який токен буде використовуватися для оплати мережевої комісії. %s</string>
<string name="fee_selector_choose_token_title">Обрати токен</string>
<string name="feed_market_and_news">Ринок та Новини</string>
<string name="feed_trending_now">В тренді</string>
<string name="feedback_data_collection_message">Інформація нижче не є обов\'язковою. Ви можете стерти її, якщо бажаєте.</string>
@ -570,6 +589,8 @@
<string name="feedback_subject_support_tangem">Звернення в підтримку Tangem</string>
<string name="feedback_subject_tx_failed">Не вдається відправити транзакцію</string>
<string name="feedback_token_description_error">Помилка в описі монети</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Недостатньо коштів</string>
<string name="gasless_transaction_fee">Комісія за транзакцію</string>
<string name="generic_error">Виникла помилка</string>
<string name="generic_error_code">Виникла помилка. Код: %s.</string>
<string name="generic_requires_memo_error">Вимагається memo</string>
@ -619,15 +640,20 @@
<string name="hw_backup_need_title">Спочатку завершіть резервне копіювання</string>
<string name="hw_backup_no_backup">Не завершено</string>
<string name="hw_backup_section_other_title">Інші методи</string>
<string name="hw_backup_seed_code_description">Збережіть фразу відновлення в безпечному місці та тримайте її в таємниці, щоб захистити свої кошти, а також налаштуйте код доступу для додаткової безпеки.</string>
<string name="hw_backup_seed_description">Збережіть фразу відновлення у безпечному місці і тримайте її у таємниці.</string>
<string name="hw_backup_seed_title">Фраза відновлення</string>
<string name="hw_backup_to_secure_description">Щоб захистити свій гаманець за допомогою коду доступу, завершіть процес резервного копіювання.</string>
<string name="hw_backup_to_upgrade_description">Щоб покращити гаманець до апаратного, спочатку створіть резервну копію.</string>
<string name="hw_backup_upgrade_description">Оновіть свій мобільний гаманець до апаратного гаманця Tangem для найвищого рівня безпеки. Імпортуйте існуючий гаманець або створіть новий.</string>
<string name="hw_backup_upgrade_title">Оновлення до холодного гаманця</string>
<string name="hw_create_keys_description">Ваші приватні ключі надійно зашифровані та зберігаються на вашому телефоні</string>
<string name="hw_create_keys_title">Ключі зберігаються у застосунку</string>
<string name="hw_create_seed_description">Створіть або відновіть свій гаманець за допомогою вашої фрази відновлення.</string>
<string name="hw_create_seed_title">Резервна копія</string>
<string name="hw_create_title">Мобільний гаманець</string>
<string name="hw_create_upgrade_description">Перемістіть свій мобільний гаманець на Tangem картку або кільце у будь який час.</string>
<string name="hw_create_upgrade_title">Оновлення до апаратного гаманця</string>
<string name="hw_import_existing_wallet">Імпортувати існуючий гаманець</string>
<string name="hw_import_seed_phrase_already_imported">Ця фраза відновлення вже була імпортована</string>
<string name="hw_mobile_wallet">Мобільний гаманець</string>
@ -821,6 +847,7 @@
<string name="markets_token_details_volume">Обсяг</string>
<string name="markets_tooltip_message">Потягніть вгору або торкніться панелі пошуку, щоб додати токени безпосередньо з маркету</string>
<string name="markets_tooltip_title">Додати токени</string>
<string name="markets_tooltip_v2_title">Додати токени</string>
<string name="markets_yield_supply_banner_description">Збільшуйте дохід з активів, зберігаючи миттєвий доступ до них. %s</string>
<string name="markets_yield_supply_banner_title">Активувати режим дохідності</string>
<string name="mobile_wallet_requires_min_os_warning_body">Оновіться до версії %1$s, щоб створити мобільний гаманець</string>
@ -1017,6 +1044,7 @@
<string name="onboarding_wallet_info_title_fourth">Відновлення коду доступу</string>
<string name="onboarding_wallet_info_title_second">Ідентичні картки</string>
<string name="onboarding_wallet_info_title_third">Код доступу</string>
<string name="only_one_mobile_wallet_explanation">Ви можете мати лише один мобільний гаманець. Оновіть його до апаратного гаманця Tangem або додайте новий апаратний гаманець.</string>
<string name="onramp_all_offers_button_title">Усі пропозиції</string>
<string name="onramp_avaiable_with_payment_methods">Доступно з %s</string>
<string name="onramp_choose_provider_title_hint">Tangem забезпечує доступ к покупці через сторонніх провайдерів згідно з їхніми умовами</string>
@ -1131,6 +1159,7 @@
<string name="reset_card_to_factory_button_title">Скинути картку</string>
<string name="reset_card_to_factory_condition_1">Я розумію, що після виконання цієї дії у мене більше не буде доступу до поточного гаманця</string>
<string name="reset_card_to_factory_condition_2">Я розумію, що не можу використати цю картку для відновлення свого коду доступу на інших картках поточного гаманця</string>
<string name="reset_card_to_factory_condition_3">Я розумію, що повністю втрачу доступ до своєї картки Tangem Pay та всіх коштів на ній без можливості відновлення.</string>
<string name="reset_card_with_backup_to_factory_message">Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки або кільця. Ви не зможете відновити поточний гаманець або використати цю картку або кільце для відновлення коду доступу.</string>
<string name="reset_card_without_backup_to_factory_message">Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки або кільця. Ви не зможете відновити поточний гаманець.</string>
<string name="reset_cards_dialog_complete_description">Усі пристрої Tangem було скинуто.</string>
@ -1279,6 +1308,11 @@
<string name="settings_card_settings_footer">Підготуйтеся до сканування кільця або картки, яку ви хочете налаштувати.</string>
<string name="settings_forget_wallet">Забути гаманець</string>
<string name="settings_forget_wallet_footer">Це призведе до видалення гаманця з застосунку. Сам гаманець можна додати знову.</string>
<string name="settings_upgrade_banner_advantage_feature_title">Простий у використанні</string>
<string name="settings_upgrade_banner_description">Зберігає ваші криптовалюти в безпеці та офлайн. Тонкий, як кредитна картка, безпечніший за банківське сховище.</string>
<string name="settings_upgrade_banner_seed_phrase_feature_title">Без seed-фрази</string>
<string name="settings_upgrade_banner_superiority_feature_title">Найкращий у своєму класі</string>
<string name="settings_upgrade_banner_title">Холодний гаманець Tangem</string>
<string name="settings_wallet_name_title">Ім\'я</string>
<string name="stake_token_description">Змусьте свій токен працювати</string>
<string name="staking_account_initialization_footer">Мережева комісія — це невелика оплата, необхідна для обробки та підтвердження вашої транзакції в блокчейні.</string>
@ -1444,6 +1478,7 @@
<string name="swap_story_third_subtitle">Просто та зручно — обмін токенів в декілька дотиків</string>
<string name="swap_story_third_title">Легше, ніж будь-коли</string>
<string name="swap_via_provider">Обмін через провайдера</string>
<string name="swap_your_assets_title">Ваші активи</string>
<string name="swapping_alert_cex_description">Сума включає: \n• комісію постачальника послуг\n• комісію мережі за відправлення %s з біржі назад на адресу користувача.</string>
<string name="swapping_alert_cex_description_with_slippage">У суму входить:\n- комісія провайдера\n- мережева комісія за відправку %1$s з біржі назад на адресу користувача. \n\nПроскакування провайдера становить до %2$s</string>
<string name="swapping_alert_dex_description">Сума включає комісію постачальника послуг.</string>
@ -1458,6 +1493,7 @@
<string name="swapping_insufficient_funds">Недостатньо коштів</string>
<string name="swapping_permission_header">Надати дозвіл</string>
<string name="swapping_swap_action">Обміняти</string>
<string name="swapping_swap_action_in_progress">Обмін...</string>
<string name="swapping_to_title">Ви отримаєте</string>
<string name="swapping_token_list_title">Оберіть токен</string>
<string name="swapping_token_not_available">недоступно</string>
@ -1476,14 +1512,18 @@
<string name="tangem_pay_freeze_card_freeze">Заморозити</string>
<string name="tangem_pay_freeze_card_success">Вашу картку заморожено.</string>
<string name="tangem_pay_get_help">Звернутися до підтримки</string>
<string name="tangem_pay_history_item_spend_mc_declined_reason">Причина: %s</string>
<string name="tangem_pay_other">Інше</string>
<string name="tangem_pay_rooted_device_subtitle">Неможливо використовувати на пристроях з root-правами.</string>
<string name="tangem_pay_status_completed">Завершено</string>
<string name="tangem_pay_status_declined">Відхилено</string>
<string name="tangem_pay_status_pending">В очікуванні</string>
<string name="tangem_pay_status_reversed">Скасовано</string>
<string name="tangem_pay_terms_fees_limits">Умови, комісії та ліміти</string>
<string name="tangem_pay_terms_limits">Умови та обмеження</string>
<string name="tangem_pay_transaction_declined_notification_text">Банк відхилив цей запит на транзакцію.</string>
<string name="tangem_pay_transaction_fee_notification_text">Ця комісія покриває витрати на обробку вашого переказу.</string>
<string name="tangem_pay_transaction_reversed_notification_text">Транзакцію було частково або повністю скасовано продавцем</string>
<string name="tangem_pay_unfreeze_card_alert_body">Продовжуйте користуватися карткою. Заморозити можна в будь-який момент.</string>
<string name="tangem_pay_unfreeze_card_alert_title">Розморозити картку?</string>
<string name="tangem_pay_unfreeze_card_failed">Не вдалося розморозити картку. Спробуйте пізніше.</string>
@ -1650,6 +1690,7 @@
<string name="universal_error">Виникла помилка. Код помилки: %s. Спробуйте, будь ласка, знову. Якщо проблема знову виникає — зверніться до нашої служби підтримки.</string>
<string name="unlock_wallet_description_full">Використовуйте %s або відскануйте картку/кільце, щоб отримати доступ до свого гаманця</string>
<string name="unsupported_wc_version">Не вдалося встановити з\'єднання: Цей dApp використовує Wallet Connect версії 1.0, яка не підтримується. Будь ласка, переконайтеся, що dApp підтримує Wallet Connect версії 2.0 для успішного підключення.</string>
<string name="upgrade_to_hardware_wallet_button_title">Оновлення до апаратного гаманця</string>
<string name="user_push_notification_agreement_argument_one">Будьте в курсі останніх функцій та новин</string>
<string name="user_push_notification_agreement_argument_one_subtitle">Миттєві сповіщення про транзакції, обміни та важливі оновлення.</string>
<string name="user_push_notification_agreement_argument_one_title">Сповіщення про транзакції</string>
@ -1797,7 +1838,18 @@
<string name="warning_button_ok">Зрозуміло!</string>
<string name="warning_button_really_cool">Дуже круто!</string>
<string name="warning_button_refresh">Оновити</string>
<string name="warning_clore_migration_button">Почати міграцію</string>
<string name="warning_clore_migration_copy_button">Копіювати</string>
<string name="warning_clore_migration_description">Щоб зберегти доступ до своїх коштів, розпочніть міграцію згідно з офіційними рекомендаціями Clore.</string>
<string name="warning_clore_migration_error_signing_not_supported">Підписання повідомлень не підтримується в цій мережі</string>
<string name="warning_clore_migration_error_wallet_manager_not_found">Неможливо підписати повідомлення. Будь ласка, спробуйте пізніше.</string>
<string name="warning_clore_migration_message">Згідно з офіційною документацією Clore, усі монети, отримані до 21 грудня, будуть мігровані в токен Clore (ERC-20); монети, отримані після цієї дати, — ні. Рішення для переказу перебуває в розробці — стежте за оновленнями.</string>
<string name="warning_clore_migration_message_label">Повідомлення</string>
<string name="warning_clore_migration_open_portal_button">Відкрити портал клейму</string>
<string name="warning_clore_migration_sheet_description">Щоб зберегти доступ до своїх активів, почніть міграцію у відповідності до офіційної інструкції Clore.</string>
<string name="warning_clore_migration_sheet_title">Міграція мережі Clore</string>
<string name="warning_clore_migration_sign_button">Підписати</string>
<string name="warning_clore_migration_signature_label">Підпис</string>
<string name="warning_clore_migration_title">Міграція мережі Clore</string>
<string name="warning_demo_mode_message">Ви перебуваєте в демонстраційному режимі</string>
<string name="warning_demo_mode_title">Демонстраційний режим активовано</string>
@ -1979,6 +2031,7 @@
<string name="welcome_create_wallet_feature_delivery">Швидка доставка</string>
<string name="welcome_create_wallet_feature_one_tap">Почніть в один клік</string>
<string name="welcome_create_wallet_feature_seamless">Просто та надійно</string>
<string name="welcome_create_wallet_feature_seedphrase">Без seed-фрази</string>
<string name="welcome_create_wallet_feature_use">Простий у використанні</string>
<string name="welcome_create_wallet_hardware_description">Створіть апаратний гаманець з Tangem. Тонкий, як банківська картка, надійний, як банківське сховище.</string>
<string name="welcome_create_wallet_mobile_description">Створіть або імпортуйте програмний гаманець</string>

View file

@ -330,10 +330,12 @@
<string name="tangem_pay_status_completed">已完成</string>
<string name="tangem_pay_status_declined">已拒絕</string>
<string name="tangem_pay_status_pending">處理中</string>
<string name="tangem_pay_status_reversed">已撤銷</string>
<string name="tangem_pay_terms_fees_limits">條款、費用與限制</string>
<string name="tangem_pay_terms_limits">條款與限制</string>
<string name="tangem_pay_transaction_declined_notification_text">銀行拒絕了此交易請求。</string>
<string name="tangem_pay_transaction_fee_notification_text">此費用用於支付處理您轉帳的成本。</string>
<string name="tangem_pay_transaction_reversed_notification_text">該交易已被商家部分或全額撤銷</string>
<string name="tangem_pay_unfreeze_card_alert_body">繼續使用您的資金。您可以隨時凍結。</string>
<string name="tangem_pay_unfreeze_card_alert_title">解凍您的卡片?</string>
<string name="tangem_pay_unfreeze_card_failed">無法解凍卡片。請稍後再試。</string>

View file

@ -1436,6 +1436,7 @@
<string name="swap_give_permission_fee_footer">The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap.</string>
<string name="swap_promo_text">Exchange more tokens at better rates directly in your wallet.</string>
<string name="swap_promo_title">New Swap Provider Available!</string>
<string name="swap_search_suggestion_hint">Looking for something else?/nTry searching or explore another crypto!</string>
<string name="swap_story_fifth_subtitle">Feel confident with round-the-clock support to help with any issues</string>
<string name="swap_story_fifth_title">Always Here</string>
<string name="swap_story_first_subtitle">Multiple trusted providers in one place—swap any asset effortlessly in your wallet</string>
@ -1489,10 +1490,12 @@
<string name="tangem_pay_status_completed">Completed</string>
<string name="tangem_pay_status_declined">Declined</string>
<string name="tangem_pay_status_pending">Pending</string>
<string name="tangem_pay_status_reversed">Reversed</string>
<string name="tangem_pay_terms_fees_limits">Terms, Fees &amp; Limits</string>
<string name="tangem_pay_terms_limits">Terms and Limits</string>
<string name="tangem_pay_transaction_declined_notification_text">The bank rejected this transaction request.</string>
<string name="tangem_pay_transaction_fee_notification_text">This fee goes to cover the cost of handling your transfer.</string>
<string name="tangem_pay_transaction_reversed_notification_text">The transaction was partially or fully reversed by the merchant</string>
<string name="tangem_pay_unfreeze_card_alert_body">Keep using your money. You can freeze anytime.</string>
<string name="tangem_pay_unfreeze_card_alert_title">Unfreeze your card?</string>
<string name="tangem_pay_unfreeze_card_failed">Failed to unfreeze the card. Try again later.</string>

View file

@ -0,0 +1,5 @@
package com.tangem.core.ui
interface HoldToConfirmButtonFeatureToggles {
val isHoldToConfirmEnabled: Boolean
}

View file

@ -21,6 +21,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.account.AccountCharIcon
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.account.AccountResIcon
@ -257,22 +258,26 @@ fun ExpandedPortfolioHeader(
}
}
val text = when (val fiatAmountState = state.fiatAmountState) {
is TokenItemState.FiatAmountState.Content -> fiatAmountState.text
is TokenItemState.FiatAmountState.TextContent -> fiatAmountState.text
else -> null
when (val fiatAmountState = state.fiatAmountState) {
is TokenItemState.FiatAmountState.Content -> FiatAmount(
text = fiatAmountState.text,
isBalanceHidden = isBalanceHidden,
modifier = balanceTextModifier,
)
is TokenItemState.FiatAmountState.TextContent -> FiatAmount(
text = fiatAmountState.text,
isBalanceHidden = isBalanceHidden,
modifier = balanceTextModifier,
)
is TokenItemState.FiatAmountState.Loading -> TextShimmer(
style = TangemTheme.typography.caption1,
textSizeHeight = true,
modifier = balanceTextModifier
.padding(horizontal = 4.dp)
.fillMaxWidth(fraction = 0.2f),
)
else -> Unit
}
Text(
text = text.orEmpty().orMaskWithStars(isBalanceHidden),
modifier = balanceTextModifier
.alignByBaseline()
.padding(horizontal = 4.dp),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
if (isCollapsable) {
@ -284,4 +289,18 @@ fun ExpandedPortfolioHeader(
)
}
}
}
@Composable
private fun RowScope.FiatAmount(text: String, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
Text(
text = text.orMaskWithStars(isBalanceHidden),
modifier = modifier
.alignByBaseline()
.padding(horizontal = 4.dp),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}

View file

@ -103,7 +103,7 @@ object DateTimeFormatters {
*/
val localFullDate: DateTimeFormatter by lazy {
val locale = Locale.getDefault()
val datePattern = DateFormat.getBestDateTimePattern(locale, "dd MMMM")
val datePattern = DateFormat.getBestDateTimePattern(locale, "d MMMM")
val timeSkeleton = if (is12HourFormat) "h:mm a" else "HH:mm"
val timePattern = DateFormat.getBestDateTimePattern(locale, timeSkeleton)
val fullPattern = "$datePattern, $timePattern"

View file

@ -30,6 +30,10 @@ class JobHolder {
job = null
}
suspend fun join() {
job?.join()
}
fun isEmpty() = job == null
}

View file

@ -10,6 +10,7 @@ internal object TangemPayTxHistoryItemStatusConverter : Converter<String, Tangem
"RESERVED" -> TangemPayTxHistoryItem.Status.RESERVED
"COMPLETED" -> TangemPayTxHistoryItem.Status.COMPLETED
"DECLINED" -> TangemPayTxHistoryItem.Status.DECLINED
"REVERSED" -> TangemPayTxHistoryItem.Status.REVERSED
else -> TangemPayTxHistoryItem.Status.UNKNOWN
}
}

View file

@ -107,15 +107,16 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
val newState = when (action) {
is WcEthTxAction.UpdateApprovalAmount -> {
val extras = uncompiled.extras as EthereumTransactionExtras
val callData = ApprovalERC20TokenCallData(
spenderAddress = uncompiled.sourceAddress,
val compiledData = extras.callData?.data ?: return
val approvalCallData = ApprovalERC20TokenCallData(compiledData) ?: return
val newApprovalCallData = approvalCallData.copy(
amount = action.amount?.amount?.let {
BlockchainAmount(currencySymbol = it.currencySymbol, decimals = it.decimals, value = it.value)
},
)
approvalAmount = action.amount
isIgnoreDAppFee = true
uncompiled.copy(extras = extras.copy(callData = callData))
uncompiled.copy(extras = extras.copy(callData = newApprovalCallData))
}
is WcEthTxAction.UpdateFee -> uncompiled.copy(fee = action.fee)
}

View file

@ -92,15 +92,16 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
val newState = when (action) {
is WcEthTxAction.UpdateApprovalAmount -> {
val extras = uncompiled.extras as EthereumTransactionExtras
val callData = ApprovalERC20TokenCallData(
spenderAddress = uncompiled.sourceAddress,
val compiledData = extras.callData?.data ?: return
val approvalCallData = ApprovalERC20TokenCallData(compiledData) ?: return
val newApprovalCallData = approvalCallData.copy(
amount = action.amount?.amount?.let {
BlockchainAmount(currencySymbol = it.currencySymbol, decimals = it.decimals, value = it.value)
},
)
approvalAmount = action.amount
isIgnoreDAppFee = true
uncompiled.copy(extras = extras.copy(callData = callData))
uncompiled.copy(extras = extras.copy(callData = newApprovalCallData))
}
is WcEthTxAction.UpdateFee -> uncompiled.copy(fee = action.fee)
}

View file

@ -31,6 +31,7 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.settings)
/** DI */
implementation(deps.hilt.android)

View file

@ -4,6 +4,7 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.settings.repositories.LegacySettingsRepository
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.repository.WalletsRepository
@ -17,18 +18,21 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DefaultHotWalletAccessor @Inject constructor(
private val tangemHotSdk: TangemHotSdk,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletPasswordRequester: HotWalletPasswordRequester,
private val walletsRepository: WalletsRepository,
private val legacySettingsRepository: LegacySettingsRepository,
dispatchers: CoroutineDispatcherProvider,
) : HotWalletAccessor {
private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io)
private var contextualUnlockHotWallet: ConcurrentHashMap<HotWalletId, UnlockHotWallet?> = ConcurrentHashMap()
private val contextualUnlockHotWallet: ConcurrentHashMap<HotWalletId, UnlockHotWallet?> = ConcurrentHashMap()
override suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> =
hotSdkRequest(hotWalletId) { unlock ->
@ -82,7 +86,7 @@ class DefaultHotWalletAccessor @Inject constructor(
}
private suspend fun <T> hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T {
val isAccessCodeRequired = walletsRepository.requireAccessCode()
val isAccessCodeRequired = isAccessCodeRequired()
val auth = when (hotWalletId.authType) {
HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth
@ -134,7 +138,7 @@ class DefaultHotWalletAccessor @Inject constructor(
}
private suspend fun updateBiometryAuthIfNeeded(hotWalletId: HotWalletId, originalAuth: HotAuth) {
val isAccessCodeRequired = walletsRepository.requireAccessCode()
val isAccessCodeRequired = isAccessCodeRequired()
val isUseBiometricAuthenticationEnabled = walletsRepository.useBiometricAuthentication()
if (originalAuth is HotAuth.Password && isUseBiometricAuthenticationEnabled && isAccessCodeRequired.not()) {
@ -167,7 +171,7 @@ class DefaultHotWalletAccessor @Inject constructor(
): T = runSuspendCatching {
block(auth)
}.getOrElse { exception ->
if (auth is HotAuth.Biometry && exception.isBiometryError()) {
if (auth is HotAuth.Biometry && (exception.isBiometryError() || exception.isBiometryReset())) {
val shouldRetryBiometry = exception is TangemSdkError.AuthenticationCanceled
// fallback to password if biometry fails
@ -215,6 +219,14 @@ class DefaultHotWalletAccessor @Inject constructor(
?: throw TangemSdkError.UserCancelled()
}
private suspend fun isAccessCodeRequired(): Boolean {
return walletsRepository.requireAccessCode() || legacySettingsRepository.canUseBiometryStrict().not()
}
private fun Throwable.isBiometryReset(): Boolean {
return this is IllegalStateException
}
private fun Throwable.isBiometryError(): Boolean {
return this is TangemSdkError.AuthenticationFailed ||
this is TangemSdkError.AuthenticationCanceled ||

View file

@ -4,5 +4,8 @@ import com.tangem.domain.settings.repositories.LegacySettingsRepository
class CanUseBiometryUseCase(private val legacySettingsRepository: LegacySettingsRepository) {
@Deprecated("You probably want to use strict() instead. Check implementation", ReplaceWith("strict()"))
suspend operator fun invoke(): Boolean = legacySettingsRepository.canUseBiometry()
suspend fun strict(): Boolean = legacySettingsRepository.canUseBiometryStrict()
}

View file

@ -3,4 +3,6 @@ package com.tangem.domain.settings.repositories
interface LegacySettingsRepository {
suspend fun canUseBiometry(): Boolean
suspend fun canUseBiometryStrict(): Boolean
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.transaction.usecase.gasless
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
@ -128,25 +129,32 @@ class GetFeeForGaslessUseCase(
} ?: raiseIllegalStateError("native currency not found for network ${network.id}")
val nativeBalance = nativeCurrencyStatus.value.amount ?: BigDecimal.ZERO
return if (nativeBalance >= feeValue) {
val nativeCoinSelectedResult =
TransactionFeeExtended(transactionFee = initialFee, feeTokenId = nativeCurrencyStatus.currency.id)
return if (nativeBalance >= feeValue) {
nativeCoinSelectedResult
} else {
findTokensToPayFee(
walletManager = walletManager,
initialTxFee = initialFee,
nativeCurrencyStatus = nativeCurrencyStatus,
networkCurrenciesStatuses = networkCurrenciesStatuses,
)
).getOrElse { error ->
when (error) {
GaslessError.NotEnoughFunds -> nativeCoinSelectedResult
else -> raise(error)
}
}
}
}
@Suppress("NullableToStringCall")
private suspend fun Raise<GetFeeError>.findTokensToPayFee(
private suspend fun findTokensToPayFee(
walletManager: EthereumWalletManager,
initialTxFee: TransactionFee,
nativeCurrencyStatus: CryptoCurrencyStatus,
networkCurrenciesStatuses: List<CryptoCurrencyStatus>,
): TransactionFeeExtended {
): Either<GetFeeError, TransactionFeeExtended> = either {
val initialFee = initialTxFee.normal as? Fee.Ethereum
?: raiseIllegalStateError(
error = "only Fee.Ethereum supported, but was ${initialTxFee.normal::class.qualifiedName}",
@ -178,6 +186,6 @@ class GetFeeForGaslessUseCase(
tokenForPayFeeStatus = tokenForPayFeeStatus,
nativeCurrencyStatus = nativeCurrencyStatus,
initialFee = initialFee,
).bind()
)
}
}

View file

@ -6,6 +6,7 @@ import arrow.core.raise.either
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.ethereum.tokenmethods.TransferERC20TokenCallData
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
@ -89,6 +90,7 @@ internal class TokenFeeCalculator(
}
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
suspend fun calculateTokenFee(
walletManager: EthereumWalletManager,
tokenForPayFeeStatus: CryptoCurrencyStatus,
@ -119,8 +121,17 @@ internal class TokenFeeCalculator(
)
val feeTransferGasLimit = when (feeTransferGasLimitResult) {
is Result.Failure -> raise(GaslessError.DataError(feeTransferGasLimitResult.error))
is Result.Success -> feeTransferGasLimitResult.data
is Result.Failure -> {
// If there is a dust on the balance, the gas limit estimation will fail with code
if (feeTransferGasLimitResult.error is BlockchainSdkError.WrappedThrowable) {
val cause = feeTransferGasLimitResult.error.cause
if (cause is BlockchainSdkError.Ethereum.InsufficientFundsForOperation) {
raise(GaslessError.NotEnoughFunds)
}
}
raise(GaslessError.DataError(feeTransferGasLimitResult.error))
}
}.increaseByPercent(PERCENT_TO_INCREASE_TRANSFER_GASLIMIT)
val baseGas = gaslessTransactionRepository.getBaseGasForTransaction()

View file

@ -72,6 +72,7 @@ sealed class TangemPayTxHistoryItem {
RESERVED,
COMPLETED,
DECLINED,
REVERSED,
UNKNOWN,
}
}

View file

@ -53,12 +53,13 @@ sealed class WcAnalyticEvents(
network: Set<Network>,
domainVerification: CheckDAppResult,
) : WcAnalyticEvents(
event = "dApp Connection Requested",
event = "DApp Connection Requested",
params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
NETWORKS to network.joinToString(",") { it.name },
DOMAIN_VERIFICATION to domainVerification.toAnalyticVerificationStatus(),
AnalyticsParam.ACCOUNT_DERIVATION to "0",
),
)

View file

@ -120,7 +120,9 @@ internal class AddTokenModel @Inject constructor(
selectedPortfolio: SelectedPortfolio,
): Boolean = coldWalletAndHasMissedDerivationsUseCase.invoke(
userWalletId = selectedPortfolio.userWallet.walletId,
networksWithDerivationPath = mapOf(selectedNetwork.selectedNetwork.networkId to null),
networksWithDerivationPath = mapOf(
selectedNetwork.selectedNetwork.networkId to selectedNetwork.cryptoCurrency.network.derivationPath.value,
),
)
private fun processError(error: Throwable?) {

View file

@ -254,7 +254,7 @@ private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
articleConfigUM = article,
onArticleClick = { relatedNews.onArticledClicked(article.id) },
modifier = articleModifier
.height(164.dp)
.heightIn(min = 164.dp)
.width(216.dp),
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
)

View file

@ -31,14 +31,12 @@ import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import javax.inject.Inject
import kotlin.coroutines.resume
@ -67,6 +65,8 @@ internal class AccessCodeModel @Inject constructor(
private val params = paramsContainer.require<AccessCodeComponent.Params>()
private val settingCodeJobHolder = JobHolder()
internal val uiState: StateFlow<AccessCodeUM>
field = MutableStateFlow(getInitialState())
@ -201,38 +201,32 @@ internal class AccessCodeModel @Inject constructor(
}
}
private fun setCode(userWalletId: UserWalletId, accessCode: String) {
private suspend fun setCode(userWalletId: UserWalletId, accessCode: String) = coroutineScope {
if (settingCodeJobHolder.isActive) {
return@coroutineScope
}
params.callbacks.onAccessCodeUpdateStarted(params.userWalletId)
modelScope.launch {
val userWallet = getUserWalletUseCase(userWalletId)
.getOrElse { error("User wallet with id $userWalletId not found") }
.requireHotWallet()
val userWallet = getUserWalletUseCase(userWalletId)
.getOrElse { error("User wallet with id $userWalletId not found") }
.requireHotWallet()
tryToAskForBiometry()
tryToAskForBiometry()
val settingCodeJob = launch(dispatchers.main) {
setCodeOperation(userWallet, accessCode)
params.callbacks.onAccessCodeUpdated(params.userWalletId)
}
val settingCodeJob = launch(dispatchers.main) {
setCodeOperation(userWallet, accessCode)
params.callbacks.onAccessCodeUpdated(params.userWalletId)
}.saveIn(settingCodeJobHolder)
setLoadingIfLongJob(settingCodeJob)
}
setLoadingIfLongJob(settingCodeJob)
}
/**
* Set access code for hot wallet
* !!! Be aware that order of operations is important here !!!
*/
private suspend fun setCodeOperation(userWallet: UserWallet.Hot, accessCode: String) {
userWalletsListRepository.setLock(
userWalletId = userWallet.walletId,
lockMethod = UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()),
)
if (walletsRepository.useBiometricAuthentication()) {
userWalletsListRepository.setLock(
userWalletId = userWallet.walletId,
lockMethod = UserWalletsListRepository.LockMethod.Biometric,
)
}
val unlockHotWallet = getHotWalletContextualUnlockUseCase(userWallet.hotWalletId)
.getOrNull()
?: run {
@ -243,22 +237,39 @@ internal class AccessCodeModel @Inject constructor(
hotWalletAccessor.unlockContextual(userWallet.hotWalletId)
}
var updatedHotWalletId = tangemHotSdk.changeAuth(
val newHotWalletIdWithPass = tangemHotSdk.changeAuth(
unlockHotWallet = unlockHotWallet,
auth = HotAuth.Password(accessCode.toCharArray()),
)
if (walletsRepository.requireAccessCode().not() && canUseBiometryUseCase()) {
updatedHotWalletId = tangemHotSdk.changeAuth(
userWalletsListRepository.saveWithoutLock(
userWallet.copy(hotWalletId = newHotWalletIdWithPass),
canOverride = true,
)
userWalletsListRepository.setLock(
userWalletId = userWallet.walletId,
lockMethod = UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()),
)
if (walletsRepository.requireAccessCode().not() && canUseBiometryUseCase.strict()) {
val newHotWalletIdWithBiometry = tangemHotSdk.changeAuth(
unlockHotWallet = unlockHotWallet,
auth = HotAuth.Biometry,
)
userWalletsListRepository.saveWithoutLock(
userWallet.copy(hotWalletId = newHotWalletIdWithBiometry),
canOverride = true,
)
}
userWalletsListRepository.saveWithoutLock(
userWallet.copy(hotWalletId = updatedHotWalletId),
canOverride = true,
)
if (walletsRepository.useBiometricAuthentication()) {
userWalletsListRepository.setLock(
userWalletId = userWallet.walletId,
lockMethod = UserWalletsListRepository.LockMethod.Biometric,
)
}
clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
}

View file

@ -219,7 +219,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
}
private suspend fun HotWalletPasswordRequester.AttemptRequest.isBiometryButtonVisible(): Boolean =
hasBiometry && canUseBiometryUseCase()
hasBiometry && canUseBiometryUseCase.strict()
private fun dismissState() {
uiState.update {

View file

@ -3,10 +3,13 @@ package com.tangem.features.hotwallet.forgetwallet.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@ -89,7 +92,13 @@ internal fun ForgetWalletContent(state: ForgetWalletUM, modifier: Modifier = Mod
@Composable
private fun CheckboxItem(checked: Boolean, onCheckedChange: () -> Unit, text: String, modifier: Modifier = Modifier) {
Row(
modifier = modifier.fillMaxWidth(),
modifier = modifier
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = { onCheckedChange() },
)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Top,
) {

View file

@ -34,13 +34,12 @@ internal sealed class CustomTokenAnalyticsEvent(
class AddTokenToAnotherAccount(
currencySymbol: String,
derivationPath: String,
source: ManageTokensSource,
) : CustomTokenAnalyticsEvent(
) : AnalyticsEvent(
category = "Settings / Account",
event = "Button - Add Token To Another Account",
params = mapOf(
AnalyticsParam.Key.TOKEN_PARAM to currencySymbol,
AnalyticsParam.Key.DERIVATION to derivationPath,
AnalyticsParam.Key.SOURCE to source.analyticsName,
),
)

View file

@ -239,7 +239,6 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
val event = CustomTokenAnalyticsEvent.AddTokenToAnotherAccount(
currencySymbol = currency.symbol,
derivationPath = currency.network.derivationPath.value.orEmpty(),
source = params.source,
)
analyticsEventHandler.send(event)
}

View file

@ -120,7 +120,9 @@ internal class AddTokenModel @Inject constructor(
selectedPortfolio: SelectedPortfolio,
): Boolean = coldWalletAndHasMissedDerivationsUseCase.invoke(
userWalletId = selectedPortfolio.userWallet.walletId,
networksWithDerivationPath = mapOf(selectedNetwork.selectedNetwork.networkId to null),
networksWithDerivationPath = mapOf(
selectedNetwork.selectedNetwork.networkId to selectedNetwork.cryptoCurrency.network.derivationPath.value,
),
)
private fun processError(error: Throwable?) {

View file

@ -50,6 +50,7 @@ dependencies {
implementation(projects.domain.account.status)
implementation(projects.domain.appTheme)
implementation(projects.domain.appTheme.models)
implementation(projects.data.common)
/** DI */
implementation(deps.hilt.android)
@ -68,6 +69,10 @@ dependencies {
implementation(deps.compose.shimmer)
implementation(deps.compose.coil)
/** Tangem libraries */
implementation(tangemDeps.blockchain)
implementation(projects.libs.blockchainSdk)
/** Other */
implementation(deps.decompose.ext.compose)
implementation(deps.kotlin.immutable.collections)

View file

@ -8,18 +8,28 @@ import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.popToFirst
import com.arkivanov.decompose.router.stack.pushNew
import com.arkivanov.decompose.router.stack.replaceAll
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.data.common.currency.getCoinId
import com.tangem.data.common.currency.getTokenId
import com.tangem.data.common.currency.isCustomCoin
import com.tangem.data.common.currency.isCustomToken
import com.tangem.data.common.network.NetworkFactory
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.derivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.GetHotCryptoUseCase
import com.tangem.domain.onramp.model.HotCryptoCurrency
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
@ -70,6 +80,7 @@ internal class HotCryptoModel @Inject constructor(
private val accountsFeatureToggles: AccountsFeatureToggles,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
val portfolioSelectorController: PortfolioSelectorController,
private val networkFactory: NetworkFactory,
private val portfolioFetcherFactory: PortfolioFetcher.Factory,
) : Model(), OnrampAddTokenComponent.Callbacks by callbackDelegate {
@ -172,10 +183,15 @@ internal class HotCryptoModel @Inject constructor(
if (isSingleAccount) {
val account = hotCryptoPortfolioData.wallet.accounts.first().account
val tokenToAdd = AddHotCryptoData(
val cryptoCurrency = updateCryptoCurrency(
cryptoCurrency = currency.cryptoCurrency,
userWallet = userWallet,
account = account,
)
val tokenToAdd = AddHotCryptoData(
cryptoCurrency = requireNotNull(cryptoCurrency),
userWallet = userWallet,
account = account,
isMorePortfolioAvailable = false,
)
hotCryptoToAddDataFlow.emit(tokenToAdd)
@ -188,10 +204,15 @@ internal class HotCryptoModel @Inject constructor(
.selectedAccountWithData(requireNotNull(portfolioFetcher))
.filterNotNull()
.map { (_, selectedAccount) ->
AddHotCryptoData(
val cryptoCurrency = updateCryptoCurrency(
cryptoCurrency = currency.cryptoCurrency,
userWallet = userWallet,
account = selectedAccount,
)
AddHotCryptoData(
cryptoCurrency = requireNotNull(cryptoCurrency),
userWallet = userWallet,
account = selectedAccount,
isMorePortfolioAvailable = true,
)
}
@ -251,6 +272,47 @@ internal class HotCryptoModel @Inject constructor(
return@isEnabled isNotAddedHotCrypto
}
}
// todo account move to common module
private fun updateCryptoCurrency(
cryptoCurrency: CryptoCurrency,
userWallet: UserWallet,
account: AccountStatus,
): CryptoCurrency? {
val derivationIndex = account.account.derivationIndex ?: return null
val blockchain = cryptoCurrency.network.toBlockchain()
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = null,
accountIndex = derivationIndex,
userWallet = userWallet,
) ?: return null
return when (cryptoCurrency) {
is CryptoCurrency.Coin -> {
val id = getCoinId(network, network.toBlockchain().toCoinId())
cryptoCurrency.copy(
id = id,
network = network,
isCustom = isCustomCoin(network),
)
}
is CryptoCurrency.Token -> {
val id = getTokenId(
network = network,
rawTokenId = cryptoCurrency.id.rawCurrencyId,
contractAddress = cryptoCurrency.contractAddress,
)
cryptoCurrency.copy(
id = id,
network = network,
isCustom = isCustomToken(id, network),
)
}
}
}
}
@ModelScoped

View file

@ -10,6 +10,8 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.BackendId
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
@ -47,10 +49,14 @@ internal class OnrampAddTokenModel @Inject constructor(
params.tokenToAdd
.distinctUntilChanged()
.mapLatest { tokenToAdd: AddHotCryptoData ->
addTokenJob.cancel()
addTokenJob.join()
val backendId = tokenToAdd.cryptoCurrency.network.backendId
val userWalletId = tokenToAdd.account.accountId.userWalletId
val isTangemIconVisible = needColdWalletInteraction(userWalletId, backendId)
val isTangemIconVisible = needColdWalletInteraction(
walletId = userWalletId,
backendId = backendId,
cryptoCurrency = tokenToAdd.cryptoCurrency,
)
uiBuilder.updateContent(
tokenToAdd = tokenToAdd,
isTangemIconVisible = isTangemIconVisible,
@ -79,7 +85,9 @@ internal class OnrampAddTokenModel @Inject constructor(
userWalletId = accountId.userWalletId,
currencyId = cryptoCurrency.id,
network = cryptoCurrency.network,
).firstOrNull()
)
.filter { (_, status) -> status.value !is CryptoCurrencyStatus.Loading }
.firstOrNull()
if (status == null) {
processError(error = null)
} else {
@ -88,11 +96,14 @@ internal class OnrampAddTokenModel @Inject constructor(
uiState.value = um.toggleProgress(false)
}
private suspend fun needColdWalletInteraction(walletId: UserWalletId, backendId: BackendId): Boolean =
coldWalletAndHasMissedDerivationsUseCase.invoke(
userWalletId = walletId,
networksWithDerivationPath = mapOf(backendId to null),
)
private suspend fun needColdWalletInteraction(
walletId: UserWalletId,
backendId: BackendId,
cryptoCurrency: CryptoCurrency,
): Boolean = coldWalletAndHasMissedDerivationsUseCase.invoke(
userWalletId = walletId,
networksWithDerivationPath = mapOf(backendId to cryptoCurrency.network.derivationPath.value),
)
private fun processError(error: Throwable?) {
val message = error?.message?.let { stringReference(it) }

View file

@ -14,9 +14,9 @@ internal class DefaultMobileWalletPromoRepository @Inject constructor(
return appPreferencesStore.getSyncOrDefault(key = SHOULD_SHOW_MOBILE_WALLET_PROMO_KEY, default = false)
}
override suspend fun setShouldShowMobileWalletPromo(value: Boolean) {
override suspend fun setShouldShowMobileWalletPromo(shouldShowPromo: Boolean) {
appPreferencesStore.editData { preferences ->
preferences[SHOULD_SHOW_MOBILE_WALLET_PROMO_KEY] = value
preferences[SHOULD_SHOW_MOBILE_WALLET_PROMO_KEY] = shouldShowPromo
}
}

View file

@ -4,5 +4,5 @@ interface MobileWalletPromoRepository {
suspend fun shouldShowMobileWalletPromo(): Boolean
suspend fun setShouldShowMobileWalletPromo(value: Boolean)
suspend fun setShouldShowMobileWalletPromo(shouldShowPromo: Boolean)
}

View file

@ -9,10 +9,10 @@ class SetShouldShowMobileWalletPromoUseCase @Inject constructor(
private val userWalletsListRepository: UserWalletsListRepository,
) {
suspend operator fun invoke(): Either<Throwable, Unit> = Either.catch {
suspend operator fun invoke(shouldShowPromo: Boolean): Either<Throwable, Unit> = Either.catch {
val wallets = userWalletsListRepository.userWallets.value
if (wallets.isNullOrEmpty()) {
mobileWalletPromoRepository.setShouldShowMobileWalletPromo(true)
mobileWalletPromoRepository.setShouldShowMobileWalletPromo(shouldShowPromo)
}
}
}

View file

@ -31,6 +31,8 @@ import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorRelo
import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents
import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents.GasPriceInserter
import com.tangem.features.send.v2.feeselector.model.transformers.*
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.transformer.update
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -62,6 +64,7 @@ internal class FeeSelectorLogic @AssistedInject constructor(
) : FeeSelectorIntents {
private var appCurrency: AppCurrency = AppCurrency.Default
private val loadFeeJobHolder = JobHolder()
val uiState = MutableStateFlow(params.state)
val isGaslessEnabled = sendFeatureToggles.isGaslessTransactionsEnabled &&
@ -113,7 +116,7 @@ internal class FeeSelectorLogic @AssistedInject constructor(
)
},
)
}
}.saveIn(loadFeeJobHolder)
}
private fun isFeeApproximate(amountType: AmountType): Boolean {

View file

@ -19,6 +19,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -118,6 +119,7 @@ internal class SendConfirmModel @Inject constructor(
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val currenciesRepository: CurrenciesRepository,
private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
sendBalanceUpdaterFactory: SendBalanceUpdater.Factory,
) : Model(), SendConfirmClickIntents, FeeSelectorModelCallback, SendNotificationsComponent.ModelCallback {
@ -597,7 +599,8 @@ internal class SendConfirmModel @Inject constructor(
val confirmUM = uiState.value.confirmUM
val isContent = confirmUM is ConfirmUM.Content
val isReadyToSend = isContent && !confirmUM.isSending
val isHoldToConfirm = userWallet.isHotWallet && isContent
val isHoldToConfirm = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled &&
userWallet.isHotWallet && isContent
return NavigationButton(
textReference = getPrimaryButtonText(confirmUM, isHoldToConfirm),
iconRes = walletInterationIcon(userWallet),

View file

@ -18,6 +18,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
@ -96,6 +97,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
private val swapAlertFactory: SwapAlertFactory,
private val appRouter: AppRouter,
private val analyticsEventHandler: AnalyticsEventHandler,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
swapTransactionSenderFactory: SwapTransactionSender.Factory,
paramsContainer: ParamsContainer,
) : Model(), FeeSelectorModelCallback, SendNotificationsComponent.ModelCallback {
@ -502,7 +504,8 @@ internal class SendWithSwapConfirmModel @Inject constructor(
val confirmUM = state.confirmUM
val isContent = confirmUM is ConfirmUM.Content
val isReadyToSend = isContent && !confirmUM.isTransactionInProcess
val isHoldToConfirm = params.userWallet.isHotWallet && isContent
val isHoldToConfirm = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled &&
params.userWallet.isHotWallet && isContent
params.callback.onResult(
route = SendWithSwapRoute.Confirm,
sendWithSwapUM = state.copy(

View file

@ -30,9 +30,11 @@ import com.tangem.features.feed.components.market.details.portfolio.add.AddToPor
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.swap.SwapComponent
import com.tangem.utils.extensions.isZero
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.math.BigDecimal
@Suppress("UnusedPrivateMember")
internal class DefaultSwapComponent @AssistedInject constructor(
@ -104,10 +106,12 @@ internal class DefaultSwapComponent @AssistedInject constructor(
val dataState by model.dataStateStateFlow.collectAsStateWithLifecycle()
val fromCryptoCurrency by remember { derivedStateOf { dataState.fromCryptoCurrency } }
val feePaidCryptoCurrency by remember { derivedStateOf { dataState.feePaidCryptoCurrency } }
val amount by remember { derivedStateOf { dataState.amount } }
val shouldHideBlock by remember {
derivedStateOf { toBigDecimalOrZero(dataState.amount).isZero() || model.uiState.isInsufficientFunds }
}
LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, amount.isNullOrBlank()) {
if (amount.isNullOrBlank()) {
LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, shouldHideBlock) {
if (shouldHideBlock) {
slotNavigation.dismiss()
return@LaunchedEffect
}
@ -201,6 +205,10 @@ internal class DefaultSwapComponent @AssistedInject constructor(
)
}
private fun toBigDecimalOrZero(bigDecimalString: String?): BigDecimal {
return bigDecimalString?.replace(",", ".")?.toBigDecimalOrNull() ?: BigDecimal.ZERO
}
@AssistedFactory
interface Factory : SwapComponent.Factory {
override fun create(context: AppComponentContext, params: SwapComponent.Params): DefaultSwapComponent

View file

@ -23,6 +23,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.InputNumberFormatter
@ -160,6 +161,7 @@ internal class SwapModel @Inject constructor(
private val getUserWalletsUseCase: GetWalletsUseCase,
private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase,
private val appsFlyerStore: AppsFlyerStore,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
) : Model() {
private val params = paramsContainer.require<SwapComponent.Params>()
@ -192,6 +194,7 @@ internal class SwapModel @Inject constructor(
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
isAccountsModeProvider = Provider { isAccountsMode },
iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork,
holdToConfirmButtonFeatureToggles = holdToConfirmButtonFeatureToggles,
)
private val inputNumberFormatter =
@ -2200,6 +2203,11 @@ internal class SwapModel @Inject constructor(
return
}
if (newState is FeeSelectorUM.Error) {
state.value = newState.copy(isHidden = true)
return
}
state.value = newState
// If fee currency is same as from currency, we need to reload quotes to update fee info

View file

@ -24,6 +24,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.domain.promo.models.StoryContent
@ -60,7 +61,7 @@ import kotlin.math.min
/**
* State builder creates a specific states for SwapScreen
*/
@Suppress("LargeClass", "TooManyFunctions")
@Suppress("LargeClass", "TooManyFunctions", "LongParameterList")
internal class StateBuilder(
private val userWalletProvider: Provider<UserWallet>,
private val actions: UiActions,
@ -68,8 +69,12 @@ internal class StateBuilder(
private val appCurrencyProvider: Provider<AppCurrency>,
private val isAccountsModeProvider: Provider<Boolean>,
private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
) {
private val isHoldToConfirmEnabled: Boolean =
holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled && userWalletProvider().isHotWallet
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
private val tokensDataConverter = TokensDataConverter(
@ -127,7 +132,7 @@ internal class StateBuilder(
swapButton = SwapButton(
walletInteractionIcon = walletInterationIcon(userWalletProvider()),
isEnabled = false,
isHoldToConfirm = userWalletProvider().isHotWallet,
isHoldToConfirm = isHoldToConfirmEnabled,
onClick = {},
),
onRefresh = {},
@ -187,7 +192,7 @@ internal class StateBuilder(
swapButton = SwapButton(
walletInteractionIcon = walletInterationIcon(userWalletProvider()),
isEnabled = false,
isHoldToConfirm = userWalletProvider().isHotWallet,
isHoldToConfirm = isHoldToConfirmEnabled,
onClick = { },
),
changeCardsButtonState = ChangeCardsButtonState.DISABLED,
@ -253,7 +258,7 @@ internal class StateBuilder(
swapButton = SwapButton(
walletInteractionIcon = walletInterationIcon(userWalletProvider()),
isEnabled = false,
isHoldToConfirm = userWalletProvider().isHotWallet,
isHoldToConfirm = isHoldToConfirmEnabled,
onClick = {},
),
providerState = ProviderState.Loading(),
@ -376,7 +381,7 @@ internal class StateBuilder(
swapButton = SwapButton(
walletInteractionIcon = walletInterationIcon(userWalletProvider()),
isEnabled = getSwapButtonEnabled(notifications),
isHoldToConfirm = userWalletProvider().isHotWallet,
isHoldToConfirm = isHoldToConfirmEnabled,
onClick = actions.onSwapClick,
),
changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible),
@ -505,7 +510,7 @@ internal class StateBuilder(
swapButton = SwapButton(
walletInteractionIcon = walletInterationIcon(userWalletProvider()),
isEnabled = false,
isHoldToConfirm = userWalletProvider().isHotWallet,
isHoldToConfirm = isHoldToConfirmEnabled,
onClick = actions.onSwapClick,
),
changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible),
@ -603,7 +608,7 @@ internal class StateBuilder(
swapButton = SwapButton(
walletInteractionIcon = walletInterationIcon(userWalletProvider()),
isEnabled = false,
isHoldToConfirm = userWalletProvider().isHotWallet,
isHoldToConfirm = isHoldToConfirmEnabled,
onClick = { },
),
changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible),

View file

@ -110,6 +110,7 @@ internal object TangemPayTxHistoryDetailsConverter :
is TangemPayTxHistoryItem.Spend -> {
val amountPrefix = when {
this.amount.isZero() -> ""
this.status == TangemPayTxHistoryItem.Status.REVERSED -> StringsSigns.MINUS
this.status == TangemPayTxHistoryItem.Status.DECLINED ||
this.amount.isPositive() -> StringsSigns.MINUS
else -> StringsSigns.PLUS
@ -205,6 +206,10 @@ internal object TangemPayTxHistoryDetailsConverter :
text = resourceReference(R.string.tangem_pay_status_declined),
style = LabelStyle.WARNING,
)
TangemPayTxHistoryItem.Status.REVERSED -> LabelUM(
text = resourceReference(R.string.tangem_pay_status_reversed),
style = LabelStyle.REGULAR,
)
TangemPayTxHistoryItem.Status.RESERVED,
TangemPayTxHistoryItem.Status.UNKNOWN,
-> null
@ -227,24 +232,33 @@ internal object TangemPayTxHistoryDetailsConverter :
containerColor = null,
)
is TangemPayTxHistoryItem.Spend -> when (this.status) {
TangemPayTxHistoryItem.Status.DECLINED ->
TangemPayTxHistoryDetailsUM.NotificationState(
config = NotificationConfig(
title = if (declinedReason.isNullOrEmpty()) {
resourceReference(R.string.tangem_pay_transaction_declined_notification_text)
} else {
resourceReference(
id = R.string.tangem_pay_history_item_spend_mc_declined_reason,
formatArgs = wrappedList(requireNotNull(declinedReason)),
)
},
subtitle = TextReference.EMPTY,
iconResId = R.drawable.ic_token_info_24,
),
titleColor = themedColor { TangemTheme.colors.text.warning },
iconTint = themedColor { TangemTheme.colors.icon.warning },
containerColor = themedColor { TangemColorPalette.Amaranth.copy(alpha = 0.1F) },
)
TangemPayTxHistoryItem.Status.DECLINED -> TangemPayTxHistoryDetailsUM.NotificationState(
config = NotificationConfig(
title = if (declinedReason.isNullOrEmpty()) {
resourceReference(R.string.tangem_pay_transaction_declined_notification_text)
} else {
resourceReference(
id = R.string.tangem_pay_history_item_spend_mc_declined_reason,
formatArgs = wrappedList(requireNotNull(declinedReason)),
)
},
subtitle = TextReference.EMPTY,
iconResId = R.drawable.ic_token_info_24,
),
titleColor = themedColor { TangemTheme.colors.text.warning },
iconTint = themedColor { TangemTheme.colors.icon.warning },
containerColor = themedColor { TangemColorPalette.Amaranth.copy(alpha = 0.1F) },
)
TangemPayTxHistoryItem.Status.REVERSED -> TangemPayTxHistoryDetailsUM.NotificationState(
config = NotificationConfig(
title = resourceReference(R.string.tangem_pay_transaction_reversed_notification_text),
subtitle = TextReference.EMPTY,
iconResId = R.drawable.ic_token_info_24,
),
titleColor = themedColor { TangemTheme.colors.text.tertiary },
iconTint = themedColor { TangemTheme.colors.icon.secondary },
containerColor = null,
)
TangemPayTxHistoryItem.Status.PENDING,
TangemPayTxHistoryItem.Status.COMPLETED,
TangemPayTxHistoryItem.Status.RESERVED,

View file

@ -37,6 +37,7 @@ internal class TangemPayTxHistoryItemsConverter(
val localDate = spend.date.withZone(DateTimeZone.getDefault())
val amountPrefix = when {
spend.amount.isZero() -> ""
spend.status == TangemPayTxHistoryItem.Status.REVERSED -> StringsSigns.MINUS
spend.status == TangemPayTxHistoryItem.Status.DECLINED || spend.amount.isPositive() -> StringsSigns.MINUS
else -> StringsSigns.PLUS
}

View file

@ -10,7 +10,7 @@ private const val NFT_COLLECTIONS_CONTENT_TYPE = "NFTCollections"
internal fun LazyListScope.nftCollections(state: WalletNFTItemUM, modifier: Modifier = Modifier) {
item(key = NFT_COLLECTIONS_CONTENT_TYPE, contentType = NFT_COLLECTIONS_CONTENT_TYPE) {
WalletNFTItem(
modifier = modifier.animateItem(),
modifier = modifier,
state = state,
)
}

View file

@ -26,7 +26,7 @@ internal fun LazyListScope.organizeTokensButton(
) {
item(key = ORGANIZE_BUTTON_CONTENT_TYPE, contentType = ORGANIZE_BUTTON_CONTENT_TYPE) {
RoundedActionButton(
modifier = modifier.animateItem().testTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON),
modifier = modifier.testTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON),
config = ActionButtonConfig(
text = resourceReference(id = R.string.organize_tokens_title),
iconResId = R.drawable.ic_filter_24,

View file

@ -16,6 +16,7 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction
import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionItemUM
import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionUM
import com.tangem.features.walletconnect.utils.WcNotificationsFactory
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
@ -26,6 +27,7 @@ internal class WcSendTransactionUMConverter @Inject constructor(
private val networkInfoUMConverter: WcNetworkInfoUMConverter,
private val requestBlockUMConverter: WcTransactionRequestBlockUMConverter,
private val notificationsFactory: WcNotificationsFactory,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
) : Converter<WcSendTransactionUMConverter.Input, WcSendTransactionUM?> {
override fun convert(value: Input): WcSendTransactionUM? {
@ -63,7 +65,8 @@ internal class WcSendTransactionUMConverter @Inject constructor(
}
},
feeErrorNotification = feeErrorNotification,
isHoldToConfirmEnabled = value.context.session.wallet.isHotWallet,
isHoldToConfirmEnabled = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled &&
value.context.session.wallet.isHotWallet,
),
feeSelectorUM = when (value.feeState) {
WcTransactionFeeState.None -> FeeSelectorUM.Loading

View file

@ -10,6 +10,7 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM
import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionItemUM
import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionUM
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
@ -19,6 +20,7 @@ internal class WcSignTransactionUMConverter @Inject constructor(
private val appInfoContentUMConverter: WcTransactionAppInfoContentUMConverter,
private val networkInfoUMConverter: WcNetworkInfoUMConverter,
private val requestBlockUMConverter: WcTransactionRequestBlockUMConverter,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
) : Converter<WcSignTransactionUMConverter.Input, WcSignTransactionUM> {
override fun convert(value: Input) = WcSignTransactionUM(
@ -36,7 +38,8 @@ internal class WcSignTransactionUMConverter @Inject constructor(
isLoading = value.signState.domainStep == WcSignStep.Signing,
address = WcAddressConverter.convert(value.context.derivationState),
walletInteractionIcon = walletInterationIcon(value.context.session.wallet),
isHoldToConfirmEnabled = value.context.session.wallet.isHotWallet,
isHoldToConfirmEnabled = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled &&
value.context.session.wallet.isHotWallet,
),
transactionRequestInfo = WcTransactionRequestInfoUM(
requestBlockUMConverter.convert(

View file

@ -10,6 +10,7 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM
import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionItemUM
import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionUM
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
@ -19,6 +20,7 @@ internal class WcSignTypedDataUMConverter @Inject constructor(
private val appInfoContentUMConverter: WcTransactionAppInfoContentUMConverter,
private val networkInfoUMConverter: WcNetworkInfoUMConverter,
private val requestBlockUMConverter: WcTransactionRequestBlockUMConverter,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
) : Converter<WcSignTypedDataUMConverter.Input, WcSignTransactionUM> {
override fun convert(value: Input): WcSignTransactionUM = WcSignTransactionUM(
@ -36,7 +38,8 @@ internal class WcSignTypedDataUMConverter @Inject constructor(
address = WcAddressConverter.convert(value.context.derivationState),
isLoading = value.signState.domainStep == WcSignStep.Signing,
walletInteractionIcon = walletInterationIcon(value.context.session.wallet),
isHoldToConfirmEnabled = value.context.session.wallet.isHotWallet,
isHoldToConfirmEnabled = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled &&
value.context.session.wallet.isHotWallet,
),
transactionRequestInfo = WcTransactionRequestInfoUM(
blocks = buildList {

View file

@ -254,7 +254,7 @@ internal class WelcomeModel @Inject constructor(
}
private suspend fun canUnlockWithBiometrics(): Boolean {
return canUseBiometryUseCase() && walletsRepository.useBiometricAuthentication()
return canUseBiometryUseCase.strict() && walletsRepository.useBiometricAuthentication()
}
private suspend fun nonBiometricUnlockWallet(userWalletId: UserWalletId) {

View file

@ -10,6 +10,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.fiat
@ -62,6 +63,7 @@ internal class YieldSupplyApproveModel @Inject constructor(
private val yieldSupplyGetContractAddressUseCase: YieldSupplyGetContractAddressUseCase,
private val yieldSupplyPendingTracker: YieldSupplyPendingTracker,
private val yieldSupplyAlertFactory: YieldSupplyAlertFactory,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
private val params: YieldSupplyApproveComponent.Params = paramsContainer.require()
@ -98,7 +100,8 @@ internal class YieldSupplyApproveModel @Inject constructor(
yieldSupplyFeeUM = YieldSupplyFeeUM.Loading,
isPrimaryButtonEnabled = false,
isTransactionSending = false,
isHoldToConfirmEnabled = params.userWallet.isHotWallet,
isHoldToConfirmEnabled = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled &&
params.userWallet.isHotWallet,
),
)

View file

@ -8,6 +8,7 @@ import com.tangem.core.analytics.models.Basic
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
@ -69,6 +70,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
private val yieldSupplyRepository: YieldSupplyRepository,
private val yieldSupplyPendingTracker: YieldSupplyPendingTracker,
private val appsFlyerStore: AppsFlyerStore,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
private val params: YieldSupplyStartEarningComponent.Params = paramsContainer.require()
@ -313,7 +315,10 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
ifRight = { wallet ->
userWallet = wallet
uiState.update {
it.copy(isHoldToConfirmEnabled = wallet.isHotWallet)
it.copy(
isHoldToConfirmEnabled = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled &&
wallet.isHotWallet,
)
}
getCurrenciesStatusUpdates()
},

View file

@ -8,6 +8,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
@ -66,6 +67,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
private val yieldSupplyRepository: YieldSupplyRepository,
private val yieldSupplyPendingTracker: YieldSupplyPendingTracker,
private val appsFlyerStore: AppsFlyerStore,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
private val params: YieldSupplyStopEarningComponent.Params = paramsContainer.require()
@ -102,7 +104,8 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
yieldSupplyFeeUM = YieldSupplyFeeUM.Loading,
isPrimaryButtonEnabled = false,
isTransactionSending = false,
isHoldToConfirmEnabled = params.userWallet.isHotWallet,
isHoldToConfirmEnabled = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled &&
params.userWallet.isHotWallet,
),
)

View file

@ -5,7 +5,7 @@
# https://github.com/tangem/tangem-sdk-android/
# https://github.com/tangem/vico
tangemBlockchainSdk = "develop-1419"
tangemBlockchainSdk = "develop-1425"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-577"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^

View file

@ -23,6 +23,7 @@ internal enum class ProviderTypeIdMapping(val id: String, val providerType: Prov
TronGrid(id = "tron", providerType = ProviderType.Tron.TronGrid),
BittensorDwellir(id = "dwellirBittensor", providerType = ProviderType.Bittensor.Dwellir),
BittensorOnfinality(id = "onfinalityBittensor", providerType = ProviderType.Bittensor.Onfinality),
AlephZeroDwellir(id = "dwellirAlephZero", providerType = ProviderType.AlephZero.Dwellir),
KoinosPro(id = "koinospro", providerType = ProviderType.Koinos.KoinosPro),
AlephiumTangem(id = "tangemAlephium", providerType = ProviderType.Alephium.Tangem),
Blink(id = "blink", providerType = ProviderType.Blink),

View file

@ -74,7 +74,7 @@ internal enum class BuildType(
BuildConfigField.LogEnabled(isEnabled = true),
BuildConfigField.TesterMenuAvailability(isEnabled = true),
BuildConfigField.MockDataSource(isEnabled = false),
BuildConfigField.ABTestsEnabled(isEnabled = true),
BuildConfigField.ABTestsEnabled(isEnabled = false),
),
),
@ -114,7 +114,7 @@ internal enum class BuildType(
BuildConfigField.LogEnabled(isEnabled = false),
BuildConfigField.TesterMenuAvailability(isEnabled = false),
BuildConfigField.MockDataSource(isEnabled = false),
BuildConfigField.ABTestsEnabled(isEnabled = true),
BuildConfigField.ABTestsEnabled(isEnabled = false),
),
),
}