diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt index 68a63f0c00..cd9e78634b 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt @@ -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), diff --git a/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt b/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt index 7463e33236..10a4807522 100644 --- a/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt +++ b/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt @@ -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 + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/core/ui/DefaultHoldToConfirmButtonFeatureToggles.kt b/app/src/main/java/com/tangem/tap/core/ui/DefaultHoldToConfirmButtonFeatureToggles.kt new file mode 100644 index 0000000000..e488d368a2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/core/ui/DefaultHoldToConfirmButtonFeatureToggles.kt @@ -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", + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/core/ui/CoreUiBindsModule.kt b/app/src/main/java/com/tangem/tap/di/core/ui/CoreUiBindsModule.kt index b02028c5bf..47e1215497 100644 --- a/app/src/main/java/com/tangem/tap/di/core/ui/CoreUiBindsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/core/ui/CoreUiBindsModule.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 2cc898457b..ce2088f11c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 20046a427d..8639981110 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -438,6 +438,7 @@ internal class DefaultTangemSdkManager( ), ) showAlert() + break } else { delay(timeMillis = 400) } diff --git a/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt b/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt index e074ddc8f8..b075775429 100644 --- a/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt @@ -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() + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index 3571b8a63c..1fab5009e0 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index 9aee042a1c..d9541b317a 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -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?>(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) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt index 16077bce72..8a28992da8 100644 --- a/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt @@ -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 } diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 52a03dcf84..301a49d9ba 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -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) + } } } } diff --git a/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt index c804791992..e83e93807b 100644 --- a/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt @@ -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, diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index ac043f69bc..399317f1ee 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -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" diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt index b9c88974ec..fab336d555 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt @@ -49,6 +49,7 @@ internal object BlockchainSDKConfigConverter : ConverterAbgeschlossen Abgelehnt Ausstehend + Storniert Bedingungen, Gebühren & Limits Bedingungen und Einschränkungen Die Bank hat diese Transaktionsanfrage abgelehnt. - Diese Gebühr dient zur Deckung der Kosten für die Abwicklung Deiner Überweisung.Questa commissione copre il costo della gestione del tuo trasferimento. + Diese Gebühr dient zur Deckung der Kosten für die Abwicklung Deiner Überweisung. + Die Transaktion wurde vom Händler teilweise oder vollständig storniert Nutze Dein Geld weiterhin. Du kannst es jederzeit einfrieren. Karte entsperren? Entsperren der Karte fehlgeschlagen. Versuchen Sie es später erneut. diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 4e0238e409..fac79c1d1f 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1481,10 +1481,12 @@ Completado Rechazado Pendiente + Revertida Términos, tarifas y límites Términos y límites El banco rechazó esta solicitud de transacción. Esta tarifa cubre el costo de procesar tu transferencia. + La transacción fue revertida parcial o totalmente por el comerciante Sigue usando tu dinero. Puedes congelarlo en cualquier momento. ¿Descongelar tu tarjeta? No se pudo descongelar la tarjeta. Inténtalo de nuevo más tarde. diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 4e92005e39..a89fafa94f 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1487,10 +1487,12 @@ Terminé Refusé En attente + Annulée Conditions, frais et limites Conditions et limites La banque a rejeté cette demande de transaction. Ces frais couvrent le coût du traitement de votre virement. + La transaction a été partiellement ou totalement annulée par le commerçant Continuez à utiliser votre argent. Vous pouvez le geler à tout moment. Dégeler votre carte ? Échec du dégel de la carte. Réessayez plus tard. diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 0c8bcde7ac..50290eaea8 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -87,10 +87,12 @@ Completato Rifiutato In sospeso + Stornata Termini, commissioni e limiti Termini e limiti La banca ha rifiutato questa richiesta di transazione. Questa commissione copre il costo della gestione del tuo trasferimento. + La transazione è stata parzialmente o totalmente stornata dal commerciante Continua a usare i tuoi soldi. Puoi congelarli in qualsiasi momento. Sbloccare la tua carta? Impossibile sbloccare la carta. Riprova più tardi. diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index c773a24141..46949f43d3 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -86,7 +86,7 @@ このアドレスには%3$s ネットワークから%1$s (%2$s) のみを送信してください。他のトークンやネットワークを使用すると、資金を失う可能性があります。 デフォルト レガシー - デバイスの生体認証をリセットするか、サポートにお問い合わせください + 生体認証で問題が発生しました。端末の生体認証をリセットするか、サポートにお問い合わせください。 認証エラー スキャン方法 サポートをリクエストする @@ -461,9 +461,15 @@ %sネットワーク 下記のみを使用して資金を送金する おすすめ + 絞り込みを解除 + リストは現在更新中のため、一時的に空になっています。しばらくしてからご確認ください。 すべてのネットワーク すべての種類 + 絞り込み + マイネットワーク + ネットワーク よく使われています + 該当する結果はありません 運用 こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。 WalletConnectエラー @@ -814,6 +820,7 @@ 取引量 これをドラッグするか、検索窓をタップして、マーケットから直接トークンを追加します トークンを追加 + トークンを追加 資産を常時アクセス可能な状態に保ったまま、パワーアップさせよう。%s 利息モードを有効にする モバイルウォレットを作成するには、%1$sにアップデートする必要があります @@ -827,7 +834,7 @@ %d分前 クイックまとめ - 関連ニュース + ニュース 関連トークン 関連ニュース 最新情報を入手 @@ -1462,10 +1469,12 @@ 完了 拒否 保留中 + 取消済み 利用規約・手数料・利用制限 利用規約と上限条件 銀行がこの取引リクエストを拒否しました。 この手数料は、送金処理にかかるコストをカバーするためのものです。 + この取引は加盟店により一部または全額取り消されました 資金は引き続き使用できます。いつでも一時停止できます。 カードの一時停止を解除しますか? カードの凍結解除に失敗しました。しばらくしてからもう一度お試しください。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 0c3f2deeb3..550f256f3f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -485,6 +485,7 @@ Все сети Все типы Фильтровать по + Мои сети Сети Часто используемые Нет результата @@ -843,6 +844,7 @@ Объем Потяните вверх или коснитесь поисковой строки, чтобы добавить токены напрямую из рынка Добавить токены + Добавить токены Увеличивайте доход с активов, сохраняя мгновенный доступ к ним. %s Активировать режим доходности Обновитесь до версии %1$s, чтобы создать мобильный кошелёк @@ -1514,10 +1516,12 @@ Успешно завершено Отклонено В процессе + Возврат Тарифы и полные условия Тарифы и лимиты Банк отклонил транзакцию Эта комиссия покрывает стоимость обработки вашего перевода. + Транзакция частично или полностью возвращена продавцом Продолжайте пользоваться картой, заморозить всегда успеете Разморозить карту? Не удалось разморозить карту, попробуйте еще раз @@ -1928,6 +1932,7 @@ Мы обнаружили неизвестную ошибку Кошелек Tangem в настоящий момент не поддерживает %s Неподдерживаемый dApp + Код ошибки: 8 005.\nЕсли проблема сохраняется — пожалуйста, свяжитесь с нашей службой поддержки. Мы обнаружили неизвестную ошибку Эта сеть %s не поддерживается Tangem Wallet и не может быть подключена. Неподдерживаемая сеть diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 4bb916c300..fa1ef254d0 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -311,6 +311,7 @@ Перейти до токену Зrozуміло Приховати + Утримуйте, щоб %s година Імпортувати В процесі @@ -337,10 +338,12 @@ Ні Немає адреси Не додано + Недоступно Не зараз Зараз ОК Відкрити в браузері + або Основна картка Основне кільце Парольна фраза @@ -386,6 +389,7 @@ Обмін Tangem Tangem Wallet + Натисніть і утримуйте умови участі Умовами використання До @@ -453,6 +457,7 @@ Цей механізм захищає картку або кільце від безконтактних атак. Між скануванням картки та виконанням команди буде додана затримка. Пароль Перед виконанням будь-якої команди, що тягне за собою зміну стану картки, вам необхідно буде ввести пароль. + Оновлення до апаратного гаманця NFT Реферальна програма Переверніть екран пристрою вниз, щоб швидко приховати та відобразити баланси @@ -466,6 +471,7 @@ Підписано Надіслати відгук Деталі + Ви можете мати лише один мобільний гаманець. Оновіть його до апаратного гаманця Tangem або додайте новий апаратний гаманець. Перевірте підключення до інтернету або змініть мережу Умови використання Основна адреса @@ -475,6 +481,15 @@ Надсилання активів в інші мережі призведе до безповоротної втрати. %s мережа Надсилайте кошти, використовуючи лише + Найкращі можливості + Очистити фільтр + Всі мережі + Всі типи + Фільтрувати за + Мої мережі + Мережі + Часто використовані + Немає результатів Привіт, команда підтримки, я зіткнувся з помилкою з кодом: %s Помилка WalletConnect Ви використали картку або кільце від іншого гаманця. Прикладіть картку або кільце, пов\'язану з цим гаманцем. @@ -555,6 +570,10 @@ ID: %s ID транзакції скопійовано Обміняйте будь-який актив у своєму портфелі на цей токен + Більш вища швидкість означає більш швидше підтвердження, але і більш вищу мережеву комісію. %s + Обрати швидкість + Оберіть, який токен буде використовуватися для оплати мережевої комісії. %s + Обрати токен Ринок та Новини В тренді Інформація нижче не є обов\'язковою. Ви можете стерти її, якщо бажаєте. @@ -570,6 +589,8 @@ Звернення в підтримку Tangem Не вдається відправити транзакцію Помилка в описі монети + Недостатньо коштів + Комісія за транзакцію Виникла помилка Виникла помилка. Код: %s. Вимагається memo @@ -619,15 +640,20 @@ Спочатку завершіть резервне копіювання Не завершено Інші методи + Збережіть фразу відновлення в безпечному місці та тримайте її в таємниці, щоб захистити свої кошти, а також налаштуйте код доступу для додаткової безпеки. Збережіть фразу відновлення у безпечному місці і тримайте її у таємниці. Фраза відновлення Щоб захистити свій гаманець за допомогою коду доступу, завершіть процес резервного копіювання. Щоб покращити гаманець до апаратного, спочатку створіть резервну копію. + Оновіть свій мобільний гаманець до апаратного гаманця Tangem для найвищого рівня безпеки. Імпортуйте існуючий гаманець або створіть новий. + Оновлення до холодного гаманця Ваші приватні ключі надійно зашифровані та зберігаються на вашому телефоні Ключі зберігаються у застосунку Створіть або відновіть свій гаманець за допомогою вашої фрази відновлення. Резервна копія Мобільний гаманець + Перемістіть свій мобільний гаманець на Tangem картку або кільце у будь який час. + Оновлення до апаратного гаманця Імпортувати існуючий гаманець Ця фраза відновлення вже була імпортована Мобільний гаманець @@ -821,6 +847,7 @@ Обсяг Потягніть вгору або торкніться панелі пошуку, щоб додати токени безпосередньо з маркету Додати токени + Додати токени Збільшуйте дохід з активів, зберігаючи миттєвий доступ до них. %s Активувати режим дохідності Оновіться до версії %1$s, щоб створити мобільний гаманець @@ -1017,6 +1044,7 @@ Відновлення коду доступу Ідентичні картки Код доступу + Ви можете мати лише один мобільний гаманець. Оновіть його до апаратного гаманця Tangem або додайте новий апаратний гаманець. Усі пропозиції Доступно з %s Tangem забезпечує доступ к покупці через сторонніх провайдерів згідно з їхніми умовами @@ -1131,6 +1159,7 @@ Скинути картку Я розумію, що після виконання цієї дії у мене більше не буде доступу до поточного гаманця Я розумію, що не можу використати цю картку для відновлення свого коду доступу на інших картках поточного гаманця + Я розумію, що повністю втрачу доступ до своєї картки Tangem Pay та всіх коштів на ній без можливості відновлення. Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки або кільця. Ви не зможете відновити поточний гаманець або використати цю картку або кільце для відновлення коду доступу. Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки або кільця. Ви не зможете відновити поточний гаманець. Усі пристрої Tangem було скинуто. @@ -1279,6 +1308,11 @@ Підготуйтеся до сканування кільця або картки, яку ви хочете налаштувати. Забути гаманець Це призведе до видалення гаманця з застосунку. Сам гаманець можна додати знову. + Простий у використанні + Зберігає ваші криптовалюти в безпеці та офлайн. Тонкий, як кредитна картка, безпечніший за банківське сховище. + Без seed-фрази + Найкращий у своєму класі + Холодний гаманець Tangem Ім\'я Змусьте свій токен працювати Мережева комісія — це невелика оплата, необхідна для обробки та підтвердження вашої транзакції в блокчейні. @@ -1444,6 +1478,7 @@ Просто та зручно — обмін токенів в декілька дотиків Легше, ніж будь-коли Обмін через провайдера + Ваші активи Сума включає: \n• комісію постачальника послуг\n• комісію мережі за відправлення %s з біржі назад на адресу користувача. У суму входить:\n- комісія провайдера\n- мережева комісія за відправку %1$s з біржі назад на адресу користувача. \n\nПроскакування провайдера становить до %2$s Сума включає комісію постачальника послуг. @@ -1458,6 +1493,7 @@ Недостатньо коштів Надати дозвіл Обміняти + Обмін... Ви отримаєте Оберіть токен недоступно @@ -1476,14 +1512,18 @@ Заморозити Вашу картку заморожено. Звернутися до підтримки + Причина: %s Інше + Неможливо використовувати на пристроях з root-правами. Завершено Відхилено В очікуванні + Скасовано Умови, комісії та ліміти Умови та обмеження Банк відхилив цей запит на транзакцію. Ця комісія покриває витрати на обробку вашого переказу. + Транзакцію було частково або повністю скасовано продавцем Продовжуйте користуватися карткою. Заморозити можна в будь-який момент. Розморозити картку? Не вдалося розморозити картку. Спробуйте пізніше. @@ -1650,6 +1690,7 @@ Виникла помилка. Код помилки: %s. Спробуйте, будь ласка, знову. Якщо проблема знову виникає — зверніться до нашої служби підтримки. Використовуйте %s або відскануйте картку/кільце, щоб отримати доступ до свого гаманця Не вдалося встановити з\'єднання: Цей dApp використовує Wallet Connect версії 1.0, яка не підтримується. Будь ласка, переконайтеся, що dApp підтримує Wallet Connect версії 2.0 для успішного підключення. + Оновлення до апаратного гаманця Будьте в курсі останніх функцій та новин Миттєві сповіщення про транзакції, обміни та важливі оновлення. Сповіщення про транзакції @@ -1797,7 +1838,18 @@ Зрозуміло! Дуже круто! Оновити + Почати міграцію + Копіювати + Щоб зберегти доступ до своїх коштів, розпочніть міграцію згідно з офіційними рекомендаціями Clore. + Підписання повідомлень не підтримується в цій мережі + Неможливо підписати повідомлення. Будь ласка, спробуйте пізніше. Згідно з офіційною документацією Clore, усі монети, отримані до 21 грудня, будуть мігровані в токен Clore (ERC-20); монети, отримані після цієї дати, — ні. Рішення для переказу перебуває в розробці — стежте за оновленнями. + Повідомлення + Відкрити портал клейму + Щоб зберегти доступ до своїх активів, почніть міграцію у відповідності до офіційної інструкції Clore. + Міграція мережі Clore + Підписати + Підпис Міграція мережі Clore Ви перебуваєте в демонстраційному режимі Демонстраційний режим активовано @@ -1979,6 +2031,7 @@ Швидка доставка Почніть в один клік Просто та надійно + Без seed-фрази Простий у використанні Створіть апаратний гаманець з Tangem. Тонкий, як банківська картка, надійний, як банківське сховище. Створіть або імпортуйте програмний гаманець diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index ad66420637..f2a8057772 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -330,10 +330,12 @@ 已完成 已拒絕 處理中 + 已撤銷 條款、費用與限制 條款與限制 銀行拒絕了此交易請求。 此費用用於支付處理您轉帳的成本。 + 該交易已被商家部分或全額撤銷 繼續使用您的資金。您可以隨時凍結。 解凍您的卡片? 無法解凍卡片。請稍後再試。 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 2d8c659c0e..55ca27cb0a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1436,6 +1436,7 @@ The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. Exchange more tokens at better rates directly in your wallet. New Swap Provider Available! + Looking for something else?/nTry searching or explore another crypto! Feel confident with round-the-clock support to help with any issues Always Here Multiple trusted providers in one place—swap any asset effortlessly in your wallet @@ -1489,10 +1490,12 @@ Completed Declined Pending + Reversed Terms, Fees & Limits Terms and Limits The bank rejected this transaction request. This fee goes to cover the cost of handling your transfer. + The transaction was partially or fully reversed by the merchant Keep using your money. You can freeze anytime. Unfreeze your card? Failed to unfreeze the card. Try again later. diff --git a/core/ui/src/main/java/com/tangem/core/ui/HoldToConfirmButtonFeatureToggles.kt b/core/ui/src/main/java/com/tangem/core/ui/HoldToConfirmButtonFeatureToggles.kt new file mode 100644 index 0000000000..35efae6bc9 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/HoldToConfirmButtonFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.core.ui + +interface HoldToConfirmButtonFeatureToggles { + val isHoldToConfirmEnabled: Boolean +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index 4b016c5544..214c54f967 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -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, + ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index 50bee9b150..95ffc567c2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -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" diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt index fa2f9a10b6..d6ac93583e 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt @@ -30,6 +30,10 @@ class JobHolder { job = null } + suspend fun join() { + job?.join() + } + fun isEmpty() = job == null } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemStatusConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemStatusConverter.kt index 775b3843a9..e1ac53a595 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemStatusConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemStatusConverter.kt @@ -10,6 +10,7 @@ internal object TangemPayTxHistoryItemStatusConverter : Converter TangemPayTxHistoryItem.Status.RESERVED "COMPLETED" -> TangemPayTxHistoryItem.Status.COMPLETED "DECLINED" -> TangemPayTxHistoryItem.Status.DECLINED + "REVERSED" -> TangemPayTxHistoryItem.Status.REVERSED else -> TangemPayTxHistoryItem.Status.UNKNOWN } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt index 4b599ee4d7..0bee94b52c 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt @@ -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) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt index e71f37533d..f471d59ba8 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt @@ -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) } diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index 5949dd0174..aafa1080db 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -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) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt index f829c76c6f..62668755ef 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt @@ -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 = ConcurrentHashMap() + private val contextualUnlockHotWallet: ConcurrentHashMap = ConcurrentHashMap() override suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List = hotSdkRequest(hotWalletId) { unlock -> @@ -82,7 +86,7 @@ class DefaultHotWalletAccessor @Inject constructor( } private suspend fun 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 || diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/CanUseBiometryUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/CanUseBiometryUseCase.kt index 880f941db8..c84d1f6361 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/CanUseBiometryUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/CanUseBiometryUseCase.kt @@ -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() } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/LegacySettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/LegacySettingsRepository.kt index e1f1b8ebb7..f37a051b33 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/LegacySettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/LegacySettingsRepository.kt @@ -3,4 +3,6 @@ package com.tangem.domain.settings.repositories interface LegacySettingsRepository { suspend fun canUseBiometry(): Boolean + + suspend fun canUseBiometryStrict(): Boolean } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetFeeForGaslessUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetFeeForGaslessUseCase.kt index 1bb6b4074c..8400491228 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetFeeForGaslessUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetFeeForGaslessUseCase.kt @@ -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.findTokensToPayFee( + private suspend fun findTokensToPayFee( walletManager: EthereumWalletManager, initialTxFee: TransactionFee, nativeCurrencyStatus: CryptoCurrencyStatus, networkCurrenciesStatuses: List, - ): TransactionFeeExtended { + ): Either = 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() + ) } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculator.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculator.kt index cec62f3d5c..451be7a94b 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculator.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculator.kt @@ -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() diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt index 9741b0d593..33289fa8f7 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt @@ -72,6 +72,7 @@ sealed class TangemPayTxHistoryItem { RESERVED, COMPLETED, DECLINED, + REVERSED, UNKNOWN, } } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt index d9f540cefb..800271cacd 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt @@ -53,12 +53,13 @@ sealed class WcAnalyticEvents( network: Set, 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", ), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt index 1d6e6d57be..d616bf53be 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt @@ -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?) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt index 434ba73d53..ff3047cac8 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt @@ -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), ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index 3ae84d85b1..9b046ff611 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -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() + private val settingCodeJobHolder = JobHolder() + internal val uiState: StateFlow 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) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index ba2bbf512a..1a2cb122b6 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -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 { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ui/ForgetWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ui/ForgetWalletContent.kt index 5dfc22b13c..ae82837b2f 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ui/ForgetWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ui/ForgetWalletContent.kt @@ -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, ) { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt index 03cab5fb6d..14aa8c4e17 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt @@ -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, ), ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index b49478560f..54e95f4260 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -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) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt index 416154863e..7f8214d8fa 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt @@ -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?) { diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 290dd27ce5..1e3239f200 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -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) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt index 75812c5cef..40ac56570b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt @@ -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 diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddTokenModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddTokenModel.kt index d786999cad..0432278b80 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddTokenModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddTokenModel.kt @@ -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) } diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/DefaultMobileWalletPromoRepository.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/DefaultMobileWalletPromoRepository.kt index 56a482e96d..1fd76e4838 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/DefaultMobileWalletPromoRepository.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/DefaultMobileWalletPromoRepository.kt @@ -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 } } diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/MobileWalletPromoRepository.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/MobileWalletPromoRepository.kt index 642ab878c7..95cba17c9d 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/MobileWalletPromoRepository.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/MobileWalletPromoRepository.kt @@ -4,5 +4,5 @@ interface MobileWalletPromoRepository { suspend fun shouldShowMobileWalletPromo(): Boolean - suspend fun setShouldShowMobileWalletPromo(value: Boolean) + suspend fun setShouldShowMobileWalletPromo(shouldShowPromo: Boolean) } \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/SetShouldShowMobileWalletPromoUseCase.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/SetShouldShowMobileWalletPromoUseCase.kt index 9049cc72bb..335669eabf 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/SetShouldShowMobileWalletPromoUseCase.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/SetShouldShowMobileWalletPromoUseCase.kt @@ -9,10 +9,10 @@ class SetShouldShowMobileWalletPromoUseCase @Inject constructor( private val userWalletsListRepository: UserWalletsListRepository, ) { - suspend operator fun invoke(): Either = Either.catch { + suspend operator fun invoke(shouldShowPromo: Boolean): Either = Either.catch { val wallets = userWalletsListRepository.userWallets.value if (wallets.isNullOrEmpty()) { - mobileWalletPromoRepository.setShouldShowMobileWalletPromo(true) + mobileWalletPromoRepository.setShouldShowMobileWalletPromo(shouldShowPromo) } } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt index b39a4b4717..a3cbaac500 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt @@ -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 { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 8f3da0e8ed..a18cc27acf 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -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), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 90526afb56..50666b4e1b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -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( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index 1ab3e27465..6a88017cb4 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -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 diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 17d79935d2..1448ec5de8 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -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() @@ -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 diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index f43c1f4caa..c3a85fe555 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -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, private val actions: UiActions, @@ -68,8 +69,12 @@ internal class StateBuilder( private val appCurrencyProvider: Provider, private val isAccountsModeProvider: Provider, 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), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt index d5be0d999b..4a1de8ef38 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt @@ -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, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt index bf0f79c163..e868de30a5 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt @@ -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 } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt index e6257855e9..ba720b16af 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt @@ -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, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt index da334b825e..94592fc246 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt @@ -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, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt index e4969a134c..6976bac3a3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt @@ -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 { 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 diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSignTransactionUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSignTransactionUMConverter.kt index 4fd75570a0..fcccd0f7f8 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSignTransactionUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSignTransactionUMConverter.kt @@ -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 { 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( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSignTypedDataUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSignTypedDataUMConverter.kt index 6f185ecfd9..5a8f8d3fc3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSignTypedDataUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSignTypedDataUMConverter.kt @@ -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 { 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 { diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index 6d0b116353..3aa198a9d7 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -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) { diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt index 4ac6a9f1ce..f817548872 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt @@ -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, ), ) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt index 1f70eff651..9c8635a26b 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -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() }, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt index 97020e9990..dd0b697ccd 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt @@ -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, ), ) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 1cc9fdda26..6d72aefb0d 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -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 ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProviderTypeIdMapping.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProviderTypeIdMapping.kt index 3c509abd97..51129751d3 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProviderTypeIdMapping.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProviderTypeIdMapping.kt @@ -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), diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index a0814f9003..0ab5e8f747 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -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), ), ), } \ No newline at end of file