From 50dca505084263f42ea2801ddb708018fece6abb Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Dec 2025 13:26:18 +0400 Subject: [PATCH 01/11] Updated on 2026-08-14 --- .../common/ui/notifications/NotificationUM.kt | 6 + core/res/src/main/res/values-de/strings.xml | 3 + core/res/src/main/res/values-ja/strings.xml | 6 +- core/res/src/main/res/values-ru/strings.xml | 26 ++ .../yield/supply/models/YieldSupplyFee.kt | 8 + .../YieldSupplyGetCurrentFeeUseCase.kt | 24 +- .../YieldSupplyGetCurrentFeeUseCaseTest.kt | 384 ++++++++++++++++++ .../active/model/YieldSupplyActiveModel.kt | 2 +- .../YieldSupplyNotificationsComponent.kt | 1 - .../entity/YieldSupplyNotificationData.kt | 1 + .../model/YieldSupplyNotificationsModel.kt | 4 + .../model/YieldSupplyStartEarningModel.kt | 3 +- 12 files changed, 460 insertions(+), 8 deletions(-) create mode 100644 domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyFee.kt create mode 100644 domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index 7425a7aa0f..fe9115cfec 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -301,6 +301,12 @@ sealed class NotificationUM(val config: NotificationConfig) { ), subtitle = TextReference.EMPTY, ) + + data object YieldSupplyHighNetworkFee : Info( + title = resourceReference(id = R.string.yield_module_high_network_fees_notification_title), + subtitle = resourceReference(id = R.string.yield_module_high_network_fees_notification_description), + iconTint = NotificationConfig.IconTint.Accent, + ) } sealed interface Cardano { diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index a009fa485f..0dc6af1380 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -849,6 +849,9 @@ Das Zielkonto verfügt nicht über eine Vertrauensstellung für das gesendete Asset. Sicher Dir 10 $ in BTC mit jeder Wallet greif schnell zu! Black Friday: bis zu 30% Nachlass + Los geht\'s + Nur für kurze Zeit! + 1+1: Beim Kauf einer Wallet erhältst Du 50% Rabatt Jetzt beitreten Teile Deinen Code – verdiene 5 USDT pro Verkauf. Deine Freunde erhalten 10 % Rabatt. Erhalte BELOHNUNGEN für jeden Freund! diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 94e4382158..82aca73289 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -848,8 +848,8 @@ ウォレットごとに$10相当のBTCをプレゼント\nお早めに! ブラックフライデー:最大30% オフ 今すぐチェック - 理想のTangemセットを揃えよう。期間限定。 - 1+1:1つ購入で、2つ目が50%オフ + 期間限定! + 1+1:ウォレット1つ購入で、2つ目が50%オフ 今すぐ参加 コードを共有すると、販売ごとに5 USDTを獲得できます。お友達は10%割引になります。 友達への紹介で報酬を獲得しよう! @@ -1391,7 +1391,7 @@ 取引を表示 サービス手数料 手数料 - カードを紛失したり盗まれたりしても、資金を安全に保てます。一時停止はいつでも解除できます。 + 資金を安全に保管します。凍結はいつでも解除できます。 カードを一時停止しますか? カードを凍結できませんでした。しばらくしてからもう一度お試しください。 一時停止 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index b5587d3945..4b22469076 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -16,8 +16,10 @@ Код доступа Превышен лимит %1$s аккаунтов. Архивируйте, чтобы создать новый. Невозможно добавить новый аккаунт. + Аккаунт заархивирован Архивированные аккаунты Восстановить + Вы собираетесь восстановить \"%1$s\". Восстановить аккаунт Лимит в 20 активных аккаунтов достигнут. Пожалуйста, зархивируйте один аккаунт, чтобы продолжить. Невозможно восстановить аккаунт @@ -26,12 +28,14 @@ Этот аккаунт участвует в реферальной программе Этот аккаунт не может быть архивирован. Мы не смогли создать аккаунт. Пожалуйста, попробуйте позже. + Аккаунт создан Архивный аккаунт Архив Вы архивируете свой аккаунт, но в любое время можете вернуть его обратно Архивация... Аккаунт Невозможно отредактировать аккаунт + Аккаунт сохранен Аккаунт для наград Добавить аккаунт Сохранить @@ -46,6 +50,8 @@ Основной аккаунт Лимит %1$s активных аккаунтов превышен. Архивируйте один, чтобы продолжить. Невозможно восстановить аккаунт + Аккаунт восстановлен + Нажмите и удерживайте аккаунт, чтобы изменить порядок аккаунтов. Продолжить Отменить Вы уверены, что хотите создание нового аккаунта? @@ -87,14 +93,17 @@ Токены в сети %1$s не поддерживаются этой картой или кольцом из-за ограничений прошивки. У вас возникли трудности со сканированием карты или кольца? Эта карта не предназначена для работы с этим приложением + Сначала установите код доступа, чтобы включить биометрию. Используйте %1$s, чтобы быстро и безопасно разблокировать кошелёк и выполнять чувствительные действия, например, подписывать транзакции. Для аппаратных кошельков всё ещё требуется карта или кольцо для подписи. Комиссия по-умолчанию Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться. Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem Включите биометрическую аутентификацию Отключение %1$s потребует ввода вашего кода доступа для разблокировки приложения и работы с кошельком. + Позже будет необходимо ввести код доступа к вашему кошельку, чтобы мы могли безопасно сохранить его для будущего использования. Все сохраненные коды доступа будут удалены. Вам потребуется вводить код доступа при работе с кошельком. При отключении функции сохранения кошелька все ранее сохраненные кошельки будут удалены из приложения. + Это удалит все сохранённые коды доступа к кошелькам. Любое дальнейшее взаимодействие с кошельком будет требовать ввода кода доступа. Требуется код доступа Эта опция отключает использование биометрии для выполнения чувствительных действий. Каждый раз, например при подписании транзакции, вам потребуется вводить код доступа. Сохранение кода доступа @@ -144,6 +153,7 @@ Вход по биометрии временно заблокирован. Попробуйте снова через 30 секунд или разблокируйте кошелёк с помощью прикладывания устройства или кода доступа. Слишком много попыток Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона. + Биометрические данные на вашем устройстве были обновлены. Пожалуйста, выберите свой кошелёк и введите его код доступа, чтобы повторно включить вход по биометрии. Внимание При обработке промокода произошла ошибка. Пожалуйста, попробуйте позже. Ошибка активации @@ -299,6 +309,7 @@ Позже Узнать больше Осталось %1$s + Легаси Bitcoin Заблокирован Заблокированные кошельки Основная сеть @@ -311,6 +322,8 @@ %d сетей %d сети + Новый адрес + Новости Далее NFT Нет @@ -363,6 +376,7 @@ Поддержка Поддерживаемые сети Обменять + Tangem условия участия Условиями использования На @@ -391,6 +405,7 @@ Адрес контракта скопирован! Доступные сети Деривация вашего токена совпадает с деривацией %1$s. Токен будет добавлен к этой учётной записи. + Деривация принадлежит другому аккаунту Токен добавлен в аккаунт %1$s Адрес контракта Адрес контракта некорректен @@ -527,6 +542,8 @@ ID: %s ID транзакции скопирован Обменяйте любой актив в вашем портфеле на этот токен + Рынок и Новости + В тренде Информация ниже не является обязательной. Вы можете стереть её, если хотите. Расскажите, каких функций вам не хватает, и мы постараемся вам помочь. Скажите, пожалуйста, какая у вас карта или кольцо? @@ -687,6 +704,7 @@ APY %s Мой портфель Рынок + Зарабатывай с Tangem Чтобы создать адреса для выбранных сетей, отсканируйте вашу карту или кольцо Tangem кошелька Потяните вверх или коснитесь поисковой строки, чтобы добавить токен Данные раздела получены из следующих сетей: %s @@ -791,6 +809,8 @@ Активировать режим доходности Обновитесь до версии %1$s, чтобы создать мобильный кошелёк Мобильный кошелек требует %1$s или новее. + Все новости + Будьте в курсе Функция NFC недоступна на вашем устройстве О NFT NFT @@ -1198,6 +1218,7 @@ %1$s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес. Сумма превышает остаток Для завершения транзакции на указанный адрес необходим тег назначения (мемо). + Требуется Destination Tag Недопустимая сумма Комиссия превышает остаток Отправляемая сумма превышает остаток @@ -1225,6 +1246,8 @@ Сетевая комиссия — это небольшая плата за обработку и подтверждение вашей транзакции в блокчейне. Чтобы начать стейкинг, ваш TON-аккаунт должен быть активирован транзакцией самому себе на 1 TON. Средства остаются в вашем кошельке — этот шаг лишь активирует ваш аккаунт для участия в стейкинге. Активация аккаунта + Сетевая комиссия изменилась. Пожалуйста, проверьте новую сумму перед продолжением. + Комиссия сети обновлена Сумма для стейкинга должна быть не менее %s Согласно правилам сети, сумма стейкинга будет округлена до %1$sTRX. Сумма для вывода из стейкинга будет округлена до %1$s TRX ввиду особенностей сети. @@ -1263,6 +1286,7 @@ Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для стейкинга. Пользуясь стейкинг сервисом, вы соглашаетесь с %1$s и %2$s Заблокировано + Максимальная сумму: %s Переместить Нативный стейкинг В данный момент нет доступных активных валидаторов для стейкинга. Пожалуйста, попробуйте позже. @@ -1598,6 +1622,7 @@ Код доступа Активация аккаунта ПИН не принят. Попробуйте ещё раз или введите другой код. + Разблокировать Выберите способ добавления кошелька Отсканируйте вашу карту или кольцо Tangem, чтобы восстановить её или импортировать из другого кошелька. Создать аппаратный кошелёк @@ -1934,6 +1959,7 @@ Политика комиссий за пополнение Tangem взимает комиссию за обслуживание в размере 15% от полученного дохода. Ваши средства будут автоматически переведены в Aave, как только комиссия сети снизится или баланс достигнет минимально необходимой суммы. + Высокая сетевая комиссия Историческая доходность Получай до %1$s APY на свой баланс Разрешение для вашего токена в режиме доходности было отозвано. Откройте токен, чтобы выдать разрешение снова. diff --git a/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyFee.kt b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyFee.kt new file mode 100644 index 0000000000..6763e16cf5 --- /dev/null +++ b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyFee.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.yield.supply.models + +import java.math.BigDecimal + +data class YieldSupplyFee( + val value: BigDecimal, + val isHighFee: Boolean = false, +) \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt index 460440f4b4..30daafec70 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt @@ -2,6 +2,8 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either import arrow.core.Either.Companion.catch +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.wallet.UserWalletId @@ -10,6 +12,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.yield.supply.YieldSupplyConst.YIELD_SUPPLY_EVM_CONSTANT_GAS_LIMIT import com.tangem.domain.yield.supply.fixFee +import com.tangem.domain.yield.supply.models.YieldSupplyFee import java.math.BigDecimal import java.math.RoundingMode @@ -25,7 +28,7 @@ class YieldSupplyGetCurrentFeeUseCase( suspend operator fun invoke( userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, - ): Either = catch { + ): Either = catch { val feeWithoutGas = feeRepository.getEthereumFeeWithoutGas(userWalletId, cryptoCurrencyStatus.currency) val fiatRate = cryptoCurrencyStatus.value.fiatRate ?: error("Fiat rate is missing") @@ -60,6 +63,23 @@ class YieldSupplyGetCurrentFeeUseCase( val tokenValue = rateRatio.multiply(nativeGas.amount.value) - tokenValue.stripTrailingZeros() + val isEthereum = cryptoCurrencyStatus.currency + .network.id.rawId.value == Blockchain.Ethereum.id + + val isHighFee = if (isEthereum) { + val maxFeePerGas = (feeWithoutGas as? Fee.Ethereum.EIP1559)?.maxFeePerGas ?: 0.toBigInteger() + maxFeePerGas >= HIGH_ETHEREUM_FEE + } else { + false + } + + YieldSupplyFee( + value = tokenValue.stripTrailingZeros(), + isHighFee = isHighFee, + ) + } + + companion object { + private val HIGH_ETHEREUM_FEE = 400_000_000.toBigInteger() } } \ No newline at end of file diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt new file mode 100644 index 0000000000..ee27141e2d --- /dev/null +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt @@ -0,0 +1,384 @@ +package com.tangem.domain.yield.supply.usecase + +import arrow.core.Either +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.quotes.QuotesRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.transaction.FeeRepository +import com.tangem.domain.utils.convertToSdkAmount +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.math.BigInteger +import java.math.RoundingMode + +@OptIn(ExperimentalCoroutinesApi::class) +class YieldSupplyGetCurrentFeeUseCaseTest { + + private val feeRepository: FeeRepository = mockk() + private val quotesRepository: QuotesRepository = mockk() + private val currenciesRepository: CurrenciesRepository = mockk() + + private lateinit var useCase: YieldSupplyGetCurrentFeeUseCase + + private val userWalletId = UserWalletId("abcdef012345") + + @BeforeEach + fun setUp() { + useCase = YieldSupplyGetCurrentFeeUseCase( + feeRepository = feeRepository, + quotesRepository = quotesRepository, + currenciesRepository = currenciesRepository, + ) + } + + @Test + fun `GIVEN valid inputs on non-ethereum WHEN invoke THEN returns fee value and not high`() = runTest { + val rawNetworkId = Blockchain.BSC.id + val tokenDecimals = 8 + val nativeDecimals = 18 + val token = createToken(rawNetworkId = rawNetworkId, decimals = tokenDecimals) + val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00")) + val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = nativeDecimals) + val nativeFiatRate = BigDecimal("4.00") + + val maxFeePerGas = BigInteger.valueOf(1_000_000_000L) + val feeWithoutGas = Fee.Ethereum.EIP1559( + maxFeePerGas = maxFeePerGas, + priorityFee = BigInteger.ONE, + gasLimit = BigInteger.ZERO, + amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus), + ) + + coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns feeWithoutGas + coEvery { + currenciesRepository.getNetworkCoin( + userWalletId = userWalletId, + networkId = token.network.id, + derivationPath = token.network.derivationPath, + ) + } returns nativeCoin + coEvery { + quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!)) + } returns setOf( + QuoteStatus( + rawCurrencyId = nativeCoin.id.rawCurrencyId!!, + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = nativeFiatRate, + priceChange = BigDecimal.ZERO, + ), + ), + ) + + val gasLimit = BigInteger.valueOf(350_000) + val nativeGasValue = maxFeePerGas.multiply(gasLimit).toBigDecimal().movePointLeft(nativeDecimals) + val rateRatio = nativeFiatRate.divide(cryptoStatus.value.fiatRate!!, tokenDecimals, RoundingMode.HALF_UP) + val expectedTokenValue = rateRatio.multiply(nativeGasValue).stripTrailingZeros() + + val result = useCase(userWalletId, cryptoStatus) + + assertThat(result.isRight()).isTrue() + val fee = (result as Either.Right).value + assertThat(fee.value).isEqualTo(expectedTokenValue) + assertThat(fee.isHighFee).isFalse() + } + + @Test + fun `GIVEN ethereum with high gas WHEN invoke THEN returns high fee flag`() = runTest { + val rawNetworkId = Blockchain.Ethereum.id + val tokenDecimals = 8 + val nativeDecimals = 18 + val token = createToken(rawNetworkId = rawNetworkId, decimals = tokenDecimals) + val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00")) + val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = nativeDecimals) + val nativeFiatRate = BigDecimal("4.00") + + val maxFeePerGas = BigInteger.valueOf(400_000_000L) // threshold + val feeWithoutGas = Fee.Ethereum.EIP1559( + maxFeePerGas = maxFeePerGas, + priorityFee = BigInteger.ONE, + gasLimit = BigInteger.ZERO, + amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus), + ) + + coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns feeWithoutGas + coEvery { + currenciesRepository.getNetworkCoin( + userWalletId = userWalletId, + networkId = token.network.id, + derivationPath = token.network.derivationPath, + ) + } returns nativeCoin + coEvery { + quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!)) + } returns setOf( + QuoteStatus( + rawCurrencyId = nativeCoin.id.rawCurrencyId!!, + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = nativeFiatRate, + priceChange = BigDecimal.ZERO, + ), + ), + ) + + val gasLimit = BigInteger.valueOf(350_000) + val nativeGasValue = maxFeePerGas.multiply(gasLimit).toBigDecimal().movePointLeft(nativeDecimals) + val rateRatio = nativeFiatRate.divide(cryptoStatus.value.fiatRate!!, tokenDecimals, RoundingMode.HALF_UP) + val expectedTokenValue = rateRatio.multiply(nativeGasValue).stripTrailingZeros() + + val result = useCase(userWalletId, cryptoStatus) + + assertThat(result.isRight()).isTrue() + val fee = (result as Either.Right).value + assertThat(fee.value).isEqualTo(expectedTokenValue) + assertThat(fee.isHighFee).isTrue() + } + + @Test + fun `GIVEN token fiat rate missing WHEN invoke THEN returns error`() = runTest { + val rawNetworkId = Blockchain.BSC.id + val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) + val cryptoStatus = createStatus(token = token, fiatRate = null) + + coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559( + maxFeePerGas = BigInteger.ONE, + priorityFee = BigInteger.ONE, + gasLimit = BigInteger.ZERO, + amount = BigDecimal.ZERO.convertToSdkAmount( + createStatus(token = token, fiatRate = BigDecimal.ONE), + ), + ) + + val result = useCase(userWalletId, cryptoStatus) + + assertThat(result.isLeft()).isTrue() + val error = (result as Either.Left).value + assertThat(error).isInstanceOf(IllegalStateException::class.java) + assertThat(error.message).isEqualTo("Fiat rate is missing") + } + + @Test + fun `GIVEN native quotes unavailable WHEN invoke THEN returns error`() = runTest { + val rawNetworkId = Blockchain.BSC.id + val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) + val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00")) + val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18) + + coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559( + maxFeePerGas = BigInteger.valueOf(1_000_000_000L), + priorityFee = BigInteger.ONE, + gasLimit = BigInteger.ZERO, + amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus), + ) + coEvery { + currenciesRepository.getNetworkCoin( + userWalletId = userWalletId, + networkId = token.network.id, + derivationPath = token.network.derivationPath, + ) + } returns nativeCoin + coEvery { quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!)) } returns null + + val result = useCase(userWalletId, cryptoStatus) + + assertThat(result.isLeft()).isTrue() + val error = (result as Either.Left).value + assertThat(error).isInstanceOf(IllegalStateException::class.java) + assertThat(error.message).isEqualTo("Quotes for native coin are unavailable") + } + + @Test + fun `GIVEN empty quotes list WHEN invoke THEN returns error`() = runTest { + val rawNetworkId = Blockchain.BSC.id + val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) + val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00")) + val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18) + + coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559( + maxFeePerGas = BigInteger.valueOf(1_000_000_000L), + priorityFee = BigInteger.ONE, + gasLimit = BigInteger.ZERO, + amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus), + ) + coEvery { + currenciesRepository.getNetworkCoin( + userWalletId = userWalletId, + networkId = token.network.id, + derivationPath = token.network.derivationPath, + ) + } returns nativeCoin + coEvery { quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!)) } returns emptySet() + + val result = useCase(userWalletId, cryptoStatus) + + assertThat(result.isLeft()).isTrue() + val error = (result as Either.Left).value + assertThat(error).isInstanceOf(IllegalStateException::class.java) + assertThat(error.message).isEqualTo("Empty quotes list for native coin") + } + + @Test + fun `GIVEN native fiat rate non-positive WHEN invoke THEN returns error`() = runTest { + val rawNetworkId = Blockchain.BSC.id + val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) + val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00")) + val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18) + + coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559( + maxFeePerGas = BigInteger.valueOf(1_000_000_000L), + priorityFee = BigInteger.ONE, + gasLimit = BigInteger.ZERO, + amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus), + ) + coEvery { + currenciesRepository.getNetworkCoin( + userWalletId = userWalletId, + networkId = token.network.id, + derivationPath = token.network.derivationPath, + ) + } returns nativeCoin + coEvery { + quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!)) + } returns setOf( + QuoteStatus( + rawCurrencyId = nativeCoin.id.rawCurrencyId!!, + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = BigDecimal.ZERO, // non-positive + priceChange = BigDecimal.ZERO, + ), + ), + ) + + val result = useCase(userWalletId, cryptoStatus) + + assertThat(result.isLeft()).isTrue() + val error = (result as Either.Left).value + assertThat(error).isInstanceOf(IllegalArgumentException::class.java) + assertThat(error.message).isEqualTo("Native fiat rate must be > 0") + } + + @Test + fun `GIVEN token fiat rate non-positive WHEN invoke THEN returns error`() = runTest { + val rawNetworkId = Blockchain.BSC.id + val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) + val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal.ZERO) // non-positive + + coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559( + maxFeePerGas = BigInteger.ONE, + priorityFee = BigInteger.ONE, + gasLimit = BigInteger.ZERO, + amount = BigDecimal.ZERO.convertToSdkAmount( + createStatus(token = token, fiatRate = BigDecimal.ONE), + ), + ) + + val result = useCase(userWalletId, cryptoStatus) + + assertThat(result.isLeft()).isTrue() + val error = (result as Either.Left).value + assertThat(error).isInstanceOf(IllegalArgumentException::class.java) + assertThat(error.message).isEqualTo("Fiat rate for token must be > 0") + } + + private fun createToken(rawNetworkId: String, decimals: Int): CryptoCurrency.Token { + val derivationPath = Network.DerivationPath.None + val network = Network( + id = Network.ID(value = rawNetworkId, derivationPath = derivationPath), + backendId = rawNetworkId, + name = rawNetworkId, + currencySymbol = rawNetworkId.take(3).uppercase(), + derivationPath = derivationPath, + isTestnet = false, + standardType = Network.StandardType.Unspecified("UNSPECIFIED"), + hasFiatFeeRate = true, + canHandleTokens = true, + transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, + ) + + return CryptoCurrency.Token( + id = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId), + suffix = CryptoCurrency.ID.Suffix.RawID(rawNetworkId), + ), + network = network, + name = "TEST_TOKEN", + symbol = "TTK", + decimals = decimals, + iconUrl = null, + isCustom = false, + contractAddress = "0xToken", + ) + } + + private fun createCoin(rawNetworkId: String, decimals: Int): CryptoCurrency.Coin { + val derivationPath = Network.DerivationPath.None + val network = Network( + id = Network.ID(value = rawNetworkId, derivationPath = derivationPath), + backendId = rawNetworkId, + name = rawNetworkId, + currencySymbol = rawNetworkId.take(3).uppercase(), + derivationPath = derivationPath, + isTestnet = false, + standardType = Network.StandardType.Unspecified("UNSPECIFIED"), + hasFiatFeeRate = true, + canHandleTokens = true, + transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, + ) + + return CryptoCurrency.Coin( + id = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId), + suffix = CryptoCurrency.ID.Suffix.RawID(rawNetworkId), + ), + network = network, + name = "TEST_COIN", + symbol = "TCN", + decimals = decimals, + iconUrl = null, + isCustom = false, + ) + } + + private fun createStatus(token: CryptoCurrency.Token, fiatRate: BigDecimal?): CryptoCurrencyStatus { + return CryptoCurrencyStatus( + currency = token, + value = CryptoCurrencyStatus.Custom( + amount = BigDecimal.ZERO, + fiatAmount = BigDecimal.ZERO, + fiatRate = fiatRate, + priceChange = BigDecimal.ZERO, + yieldBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x0000000000000000000000000000000000000000", + type = NetworkAddress.Address.Type.Primary, + ), + ), + sources = CryptoCurrencyStatus.Sources(), + ), + ) + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt index d468e6d60d..e18a49c84a 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt @@ -283,7 +283,7 @@ internal class YieldSupplyActiveModel @Inject constructor( YieldSupplyActiveFeeContentTransformer( cryptoCurrencyStatus = cryptoStatus, appCurrency = appCurrency, - feeValue = currentFee, + feeValue = currentFee.value, maxNetworkFee = maxFee, analyticsHandler = analyticsHandler, ), diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt index 366490ead7..0e5ade929b 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt @@ -43,7 +43,6 @@ internal class YieldSupplyNotificationsComponent( Modifier.padding(top = 16.dp) }, containerColor = TangemTheme.colors.background.action, - iconTint = null, ) } } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/entity/YieldSupplyNotificationData.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/entity/YieldSupplyNotificationData.kt index 7b9a1e53ef..446047a107 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/entity/YieldSupplyNotificationData.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/entity/YieldSupplyNotificationData.kt @@ -6,4 +6,5 @@ import java.math.BigDecimal data class YieldSupplyNotificationData( val feeValue: BigDecimal?, val feeError: GetFeeError?, + val shouldShowHighFeeNotification: Boolean = false, ) \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt index 77907f4839..2e81b51d3f 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt @@ -74,6 +74,10 @@ internal class YieldSupplyNotificationsModel @Inject constructor( dustValue = null, onReload = params.callback::onFeeReload, ) + + if (data.shouldShowHighFeeNotification) { + add(NotificationUM.Info.YieldSupplyHighNetworkFee) + } } if (notifications.any { it is NotificationUM.Error.TokenExceedsBalance }) { 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 a41f836310..6a2d3d2597 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 @@ -190,7 +190,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( updatedTransactionList = updatedTransactionList, feeValue = feeSum, maxNetworkFee = maxFee, - estimatedFeeValueInTokenCurrency = estimatedFee, + estimatedFeeValueInTokenCurrency = estimatedFee.value, minAmount = minAmount, ), ) @@ -198,6 +198,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( data = YieldSupplyNotificationData( feeValue = feeSum, feeError = null, + shouldShowHighFeeNotification = estimatedFee.isHighFee, ), ) }, From 1a9e2006d5505f373570be6bcf80d7ebc38b687b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Dec 2025 14:01:49 +0300 Subject: [PATCH 02/11] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- .../kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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 03e0a8cfd3..963f847260 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 @@ -17,7 +17,7 @@ }, { "name": "STAKING_CARDANO_ENABLED", - "version": "undefined" + "version": "5.31.1" }, { "name": "STAKING_ETH_ENABLED", diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt index 4423dddf32..919f80982d 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt @@ -1,5 +1,6 @@ package com.tangem.data.pay.util +import com.tangem.common.card.FirmwareVersion import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -32,7 +33,7 @@ class TangemPayWalletsManager @Inject constructor( private fun findColdWallet(userWallets: List?): UserWallet.Cold { return userWallets?.find { - it is UserWallet.Cold && it.isMultiCurrency + it is UserWallet.Cold && it.scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable } as? UserWallet.Cold ?: error("Cannot find cold user wallet") } } \ No newline at end of file From 72d32d9254f48fa44afc24e6857b7d1ced24bd7a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Dec 2025 09:42:12 +0300 Subject: [PATCH 03/11] Updated on 2026-08-14 --- .../tap/di/domain/OnrampDomainModule.kt | 3 + .../tangem/datasource/di/PromoStoreModule.kt | 9 ++ .../local/promo/DefaultPromoBannerStore.kt | 19 ++++ .../local/promo/PromoBannerStore.kt | 10 ++ .../data/promo/DefaultPromoRepository.kt | 15 +++ .../tangem/data/promo/di/PromoDataModule.kt | 3 + domain/onramp/build.gradle.kts | 1 + .../domain/onramp/GetOnrampOffersUseCase.kt | 35 +++++- .../onramp/GetOnrampOffersUseCaseTest.kt | 105 ++++++++++++++++++ .../tangem/domain/promo/PromoRepository.kt | 2 + tangem-android-tools | 2 +- 11 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index 0c20fd0993..34ffd4aaba 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.onramp.* import com.tangem.domain.onramp.repositories.* +import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.repositories.SettingsRepository import dagger.Module import dagger.Provides @@ -264,12 +265,14 @@ internal object OnrampDomainModule { onrampErrorResolver: OnrampErrorResolver, onrampTransactionRepository: OnrampTransactionRepository, settingsRepository: SettingsRepository, + promoRepository: PromoRepository, ): GetOnrampOffersUseCase { return GetOnrampOffersUseCase( onrampRepository = onrampRepository, errorResolver = onrampErrorResolver, onrampTransactionRepository = onrampTransactionRepository, settingsRepository = settingsRepository, + promoRepository = promoRepository, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt index b8af7137ae..af846ebcbb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt @@ -1,7 +1,10 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.promo.DefaultPromoBannerStore import com.tangem.datasource.local.promo.DefaultPromoStoriesStore +import com.tangem.datasource.local.promo.PromoBannerStore import com.tangem.datasource.local.promo.PromoStoriesStore import dagger.Module import dagger.Provides @@ -18,4 +21,10 @@ object PromoStoreModule { fun providePromoStoriesStore(): PromoStoriesStore { return DefaultPromoStoriesStore(dataStore = RuntimeDataStore()) } + + @Provides + @Singleton + fun providePromoBannerStore(): PromoBannerStore { + return DefaultPromoBannerStore(dataStore = RuntimeSharedStore()) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt new file mode 100644 index 0000000000..6065fa079a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt @@ -0,0 +1,19 @@ +package com.tangem.datasource.local.promo + +import com.tangem.datasource.api.promotion.models.PromoBannerResponse +import com.tangem.datasource.local.datastore.RuntimeSharedStore + +internal class DefaultPromoBannerStore( + private val dataStore: RuntimeSharedStore>, +) : PromoBannerStore { + + override suspend fun getSyncOrNull(promoId: String): PromoBannerResponse? { + return dataStore.getSyncOrNull()?.get(promoId) + } + + override suspend fun store(promoId: String, promoBanner: PromoBannerResponse) { + dataStore.update(emptyMap()) { current -> + current + (promoId to promoBanner) + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt new file mode 100644 index 0000000000..f951238bd5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.promo + +import com.tangem.datasource.api.promotion.models.PromoBannerResponse + +interface PromoBannerStore { + + suspend fun getSyncOrNull(promoId: String): PromoBannerResponse? + + suspend fun store(promoId: String, promoBanner: PromoBannerResponse) +} \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index d6041cb705..fe58a685da 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -10,6 +10,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowStor import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.store +import com.tangem.datasource.local.promo.PromoBannerStore import com.tangem.datasource.local.promo.PromoStoriesStore import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.promo.PromoRepository @@ -27,6 +28,7 @@ internal class DefaultPromoRepository( private val tangemApi: TangemTechApi, private val appPreferencesStore: AppPreferencesStore, private val promoStoriesStore: PromoStoriesStore, + private val promoBannerStore: PromoBannerStore, private val dispatchers: CoroutineDispatcherProvider, private val referralRepository: ReferralRepository, ) : PromoRepository { @@ -101,6 +103,18 @@ internal class DefaultPromoRepository( ) } + override suspend fun isMoonpayPromoActive(): Boolean { + val banner = runCatching(dispatchers.io) { + val response = promoBannerStore.getSyncOrNull(MOONPAY_NAME) ?: run { + val apiResponse = tangemApi.getPromoBanner(MOONPAY_NAME).getOrThrow() + promoBannerStore.store(MOONPAY_NAME, apiResponse) + apiResponse + } + promoBannerConverter.convert(response) + }.getOrNull() + return banner?.isActive == true + } + override fun getStoryById(id: String): Flow = isReadyToShowStories(id).mapLatest { getStoryByIdSync(id = id, refresh = false) } @@ -180,6 +194,7 @@ internal class DefaultPromoRepository( const val VISA_NAME = "visa-waitlist" const val BLACK_FRIDAY_NAME = "black-friday" const val ONE_PLUS_ONE_NAME = "one-plus-one" + const val MOONPAY_NAME = "moonpay" const val STORIES_LOAD_DELAY = 1000L } } \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt b/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt index 69d489a2dc..2f0c689e53 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt @@ -3,6 +3,7 @@ package com.tangem.data.promo.di import com.tangem.data.promo.DefaultPromoRepository import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.promo.PromoBannerStore import com.tangem.datasource.local.promo.PromoStoriesStore import com.tangem.domain.promo.PromoRepository import com.tangem.feature.referral.domain.ReferralRepository @@ -23,6 +24,7 @@ internal object PromoDataModule { tangemTechApi: TangemTechApi, appPreferencesStore: AppPreferencesStore, promoStoriesStore: PromoStoriesStore, + promoBannerStore: PromoBannerStore, dispatchers: CoroutineDispatcherProvider, referralRepository: ReferralRepository, ): PromoRepository { @@ -32,6 +34,7 @@ internal object PromoDataModule { promoStoriesStore = promoStoriesStore, dispatchers = dispatchers, referralRepository = referralRepository, + promoBannerStore = promoBannerStore, ) } } \ No newline at end of file diff --git a/domain/onramp/build.gradle.kts b/domain/onramp/build.gradle.kts index 7823e8dcea..cd1f511092 100644 --- a/domain/onramp/build.gradle.kts +++ b/domain/onramp/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { api(projects.domain.core) api(projects.domain.settings) implementation(deps.kotlin.serialization) + implementation(projects.domain.promo) /** Tests */ testImplementation(deps.test.coroutine) diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt index 0b2aba9380..8bc240224a 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt @@ -11,9 +11,11 @@ import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository import com.tangem.domain.onramp.utils.calculateRateDif import com.tangem.domain.onramp.utils.compareOffersByRateSpeedAndPriority +import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.repositories.SettingsRepository import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map class GetOnrampOffersUseCase( @@ -21,14 +23,16 @@ class GetOnrampOffersUseCase( private val onrampTransactionRepository: OnrampTransactionRepository, private val errorResolver: OnrampErrorResolver, private val settingsRepository: SettingsRepository, + private val promoRepository: PromoRepository, ) { operator fun invoke(): EitherFlow> { return combine( onrampRepository.getQuotes(), onrampTransactionRepository.getAllTransactions(), - ) { quotes, transactions -> - processOffers(quotes, transactions) + flow { emit(promoRepository.isMoonpayPromoActive()) }, + ) { quotes, transactions, isMoonpayPromoActive -> + processOffers(quotes, transactions, isMoonpayPromoActive) } .map { offers -> offers.right() } .catch { throwable -> errorResolver.resolve(throwable).left() } @@ -37,6 +41,7 @@ class GetOnrampOffersUseCase( private suspend fun processOffers( quotes: List, transactions: List, + isMoonpayPromoActive: Boolean, ): List { val validQuotes = quotes.filterIsInstance() if (validQuotes.isEmpty()) return emptyList() @@ -57,7 +62,7 @@ class GetOnrampOffersUseCase( val recentOffer = findRecentOffer(offers, transactions) val bestRateOffer = findBestRateOffer(offers, isGooglePayAvailable) - val fastestOffer = findFastestOffer(offers, isGooglePayAvailable) + val fastestOffer = findFastestOffer(offers, isGooglePayAvailable, isMoonpayPromoActive) return buildOffersBlocks( recentOffer = recentOffer, @@ -85,8 +90,24 @@ class GetOnrampOffersUseCase( return offers.maxWithOrNull(offerComparator(isGooglePayAvailable)) } - private fun findFastestOffer(offers: List, isGooglePayAvailable: Boolean): OnrampOffer? { - val instantOffers = offers.filter { it.quote.paymentMethod.type.isInstant() } + private fun findFastestOffer( + offers: List, + isGooglePayAvailable: Boolean, + isMoonpayPromoActive: Boolean, + ): OnrampOffer? { + val moonpayPromoOffers = if (isMoonpayPromoActive) { + offers.filter { + it.quote.provider.id == MOONPAY_PROMO_PROVIDER_ID && + it.quote.paymentMethod.type == PaymentMethodType.GOOGLE_PAY + } + } else { + emptyList() + } + + val instantOffers = moonpayPromoOffers.ifEmpty { + offers.filter { it.quote.paymentMethod.type.isInstant() } + } + return if (instantOffers.isNotEmpty()) { instantOffers.maxWithOrNull(fastestOfferComparator(isGooglePayAvailable)) } else { @@ -287,4 +308,8 @@ class GetOnrampOffersUseCase( -> true } } + + private companion object { + const val MOONPAY_PROMO_PROVIDER_ID = "moonpay" + } } \ No newline at end of file diff --git a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt index 660735a8e7..5f249f97a2 100644 --- a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt +++ b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt @@ -7,6 +7,7 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository +import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.repositories.SettingsRepository import io.mockk.* import kotlinx.coroutines.flow.flowOf @@ -24,6 +25,7 @@ class GetOnrampOffersUseCaseTest { private val errorResolver: OnrampErrorResolver = mockk(relaxUnitFun = true) private val settingsRepository: SettingsRepository = mockk(relaxUnitFun = true) private val cryptoCurrencyId: CryptoCurrency.ID = mockk(relaxUnitFun = true) + private val promoRepository: PromoRepository = mockk(relaxUnitFun = true) private lateinit var useCase: GetOnrampOffersUseCase @@ -35,6 +37,7 @@ class GetOnrampOffersUseCaseTest { onrampTransactionRepository = onrampTransactionRepository, errorResolver = errorResolver, settingsRepository = settingsRepository, + promoRepository = promoRepository, ) } @@ -225,6 +228,7 @@ class GetOnrampOffersUseCaseTest { val transactions = emptyList() + coEvery { promoRepository.isMoonpayPromoActive() } returns false coEvery { settingsRepository.isGooglePayAvailability() } returns false coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf( @@ -245,6 +249,107 @@ class GetOnrampOffersUseCaseTest { } } + @Test + fun `invoke should fallback to standard instant offers when promo is active but no Moonpay offers exist`() = + runTest { + val instantMethod = createMockPaymentMethod("gpay", "Google Pay", PaymentMethodType.GOOGLE_PAY) + val slowMethod = createMockPaymentMethod("bank", "Bank Transfer", PaymentMethodType.CARD) + val provider = createMockProvider("other", "Other Provider") + + val quotes = listOf( + createMockQuote(instantMethod, provider, BigDecimal("95.0")), + createMockQuote(slowMethod, provider, BigDecimal("100.0")), + ) + + val transactions = emptyList() + + coEvery { promoRepository.isMoonpayPromoActive() } returns true + coEvery { settingsRepository.isGooglePayAvailability() } returns true + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf(transactions) + + val result = useCase() + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended } + Truth.assertThat(recommendedBlock).isNotNull() + Truth.assertThat(recommendedBlock?.offers).hasSize(2) + + val fastestOffer = + recommendedBlock?.offers?.find { it.advantages == OnrampOfferAdvantages.Fastest } + Truth.assertThat(fastestOffer).isNotNull() + + when (val quote = fastestOffer?.quote) { + is OnrampQuote.Data -> { + Truth.assertThat(quote.provider.id).isNotEqualTo("moonpay") + Truth.assertThat(quote.paymentMethod.type).isEqualTo(PaymentMethodType.GOOGLE_PAY) + } + else -> Truth.assertThat(false).isTrue() + } + }, + ) + } + } + + @Test + fun `invoke should show Moonpay fastest offer when promo is active`() = runTest { + val moonpayGooglePayMethod = createMockPaymentMethod("moonpay-gpay", "Google Pay", PaymentMethodType.GOOGLE_PAY) + val otherGooglePayMethod = createMockPaymentMethod( + "other-gpay", + "Other Google Pay", + PaymentMethodType.GOOGLE_PAY, + ) + val slowMethod = createMockPaymentMethod("bank", "Bank Transfer", PaymentMethodType.CARD) + val moonpayProvider = createMockProvider("moonpay", "Moonpay") + val otherProvider = createMockProvider("other", "Other Provider") + + val quotes = listOf( + createMockQuote(moonpayGooglePayMethod, moonpayProvider, BigDecimal("100.0")), + createMockQuote(otherGooglePayMethod, otherProvider, BigDecimal("95.0")), + createMockQuote(slowMethod, otherProvider, BigDecimal("105.0")), + ) + + val transactions = emptyList() + + coEvery { promoRepository.isMoonpayPromoActive() } returns true + coEvery { settingsRepository.isGooglePayAvailability() } returns true + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf(transactions) + + val result = useCase() + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended } + Truth.assertThat(recommendedBlock).isNotNull() + + val fastestOffer = recommendedBlock?.offers?.find { it.advantages == OnrampOfferAdvantages.Fastest } + Truth.assertThat(fastestOffer).isNotNull() + + when (val quote = fastestOffer?.quote) { + is OnrampQuote.Data -> { + Truth.assertThat(quote.provider.id).isEqualTo("moonpay") + Truth.assertThat(quote.paymentMethod.type).isEqualTo(PaymentMethodType.GOOGLE_PAY) + Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("100.0")) + } + else -> Truth.assertThat(false).isTrue() + } + }, + ) + } + } + private fun createMockPaymentMethod( id: String, name: String, diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt index 18d7a50ec4..db6aa18461 100644 --- a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt @@ -19,6 +19,8 @@ interface PromoRepository { suspend fun isMarketsStakingNotificationHideClicked(): Flow suspend fun setMarketsStakingNotificationHideClicked() + + suspend fun isMoonpayPromoActive(): Boolean // endregion // region Stories diff --git a/tangem-android-tools b/tangem-android-tools index 3da4c865da..d7950d60bb 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 3da4c865da5f2a61d5170357c60de4ddd8425815 +Subproject commit d7950d60bb4c6353f2aa3364073c6ec72167c666 From c551b2dea9a4e32b156f29833c6c90087620a082 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Dec 2025 16:12:39 +0200 Subject: [PATCH 04/11] Updated on 2026-08-14 --- .../notifications/AddStakingNotificationsTransformer.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index 66d3c56eac..5968fa4437 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -291,6 +291,8 @@ internal class AddStakingNotificationsTransformer( } private fun MutableList.addTonInitializeAccountNotification(prevState: StakingUiState) { + if (prevState.actionType !is StakingActionCommonType.Enter) return + val isAccountInitialized = isAccountInitializedProvider.invoke() val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId From 938c8e72cb9b2458d833d1d32252e8c2ac86eb1b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Dec 2025 19:45:24 +0500 Subject: [PATCH 05/11] Updated on 2026-08-14 --- .../tangem/data/swap/DefaultSwapRepositoryV2.kt | 4 ++-- .../com/tangem/domain/swap/SwapRepositoryV2.kt | 4 ++-- .../swap/usecase/GetSwapSupportedPairsUseCase.kt | 5 ++++- .../swap/usecase/SwapTransactionSentUseCase.kt | 3 ++- .../confirm/model/SwapTransactionSender.kt | 10 ++++++++++ .../feature/swap/domain/SwapInteractorImpl.kt | 16 ++++++++++++++-- 6 files changed, 34 insertions(+), 8 deletions(-) diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index 343ba4d4bd..56a6d621fb 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -272,7 +272,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( override suspend fun swapTransactionSent( userWallet: UserWallet, fromCryptoCurrencyStatus: CryptoCurrencyStatus, - toAddress: String, + payInAddress: String, txId: String, txHash: String, txExtraId: String?, @@ -288,7 +288,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( txId = txId, fromNetwork = fromCryptoCurrencyStatus.currency.network.backendId, fromAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), - payinAddress = toAddress, + payinAddress = payInAddress, payinExtraId = txExtraId, txHash = txHash, ), diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt index d25708c229..8f26ccadb6 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -93,7 +93,7 @@ interface SwapRepositoryV2 { * * @param userWallet selected user wallet * @param fromCryptoCurrencyStatus currency status being swapped from - * @param toAddress swap destination address + * @param payInAddress swap destination address * @param txId transaction id in ExpressApi * @param txHash transaction hash in blockchain * @param txExtraId extra transaction id in ExpressApi @@ -101,7 +101,7 @@ interface SwapRepositoryV2 { suspend fun swapTransactionSent( userWallet: UserWallet, fromCryptoCurrencyStatus: CryptoCurrencyStatus, - toAddress: String, + payInAddress: String, txId: String, txHash: String, txExtraId: String?, diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt index 7cd009b31b..af3d4fe6aa 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt @@ -32,7 +32,10 @@ class GetSwapSupportedPairsUseCase( swapTxType = swapTxType, ) - val filteredOutInitial = cryptoCurrencyList.filterNot { it.id.rawNetworkId == initialCurrency.id.rawNetworkId } + val filteredOutInitial = cryptoCurrencyList.filterNot { currency -> + currency.id.rawNetworkId == initialCurrency.id.rawNetworkId && + currency.id.rawCurrencyId == initialCurrency.id.rawCurrencyId + } val fromGroup = pairs.groupPairs( initialCurrency = initialCurrency, diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt index be872f8701..92fcc325e3 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt @@ -27,6 +27,7 @@ class SwapTransactionSentUseCase( swapDataTransactionModel: SwapDataTransactionModel, provider: ExpressProvider, txHash: String, + payInAddress: String, timestamp: Long, swapTxType: SwapTxType, ) = Either.catch { @@ -63,7 +64,7 @@ class SwapTransactionSentUseCase( swapRepositoryV2.swapTransactionSent( userWallet = userWallet, fromCryptoCurrencyStatus = fromCryptoCurrencyStatus, - toAddress = swapDataTransactionModel.txTo, + payInAddress = payInAddress, txId = swapDataTransactionModel.txId, txHash = txHash, txExtraId = swapDataTransactionModel.txExtraId, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt index 3602881267..1953adbd19 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt @@ -1,7 +1,9 @@ package com.tangem.features.swap.v2.impl.sendviaswap.confirm.model import arrow.core.getOrElse +import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplySendCallData import com.tangem.domain.express.models.ExpressError import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.express.models.ExpressProvider @@ -152,6 +154,13 @@ internal class SwapTransactionSender @AssistedInject constructor( return } + val ethereumCallData = (txData.extras as? EthereumTransactionExtras)?.callData + val payInAddress = if (ethereumCallData is EthereumYieldSupplySendCallData) { + ethereumCallData.destinationAddress + } else { + txData.destinationAddress + } + sendTransactionUseCase( txData = txData, userWallet = userWallet, @@ -169,6 +178,7 @@ internal class SwapTransactionSender @AssistedInject constructor( swapDataTransactionModel = swapTransaction, provider = provider, txHash = txHash, + payInAddress = payInAddress, timestamp = timestamp, swapTxType = SwapTxType.SendWithSwap, ) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index aa8415a3c9..0531735446 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -3,6 +3,7 @@ package com.tangem.feature.swap.domain import android.util.Base64 import arrow.core.Either import arrow.core.getOrElse +import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.solana.SolanaTransactionHelper import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain @@ -11,6 +12,7 @@ import com.tangem.blockchain.common.TransactionExtras import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplySendCallData import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId @@ -870,6 +872,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( Timber.e(it, "Failed to create swap dex tx data") return SwapTransactionState.Error.UnknownError } + return handleSwapResult( provider = provider, networkId = networkId, @@ -881,7 +884,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( amount = amount, derivationPath = derivationPath, txData = txData, - payInAddress = txData.destinationAddress, + payInAddress = getPayoutAddress(txData), ) } @@ -1087,7 +1090,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( txId = exchangeDataCex.txId, fromNetwork = currencyToSend.currency.network.backendId, fromAddress = currencyToSend.value.networkAddress?.defaultAddress?.value.orEmpty(), - payInAddress = txData.destinationAddress, + payInAddress = getPayoutAddress(txData), txHash = txHash, payInExtraId = exchangeDataCex.txExtraId, ) @@ -2307,6 +2310,15 @@ internal class SwapInteractorImpl @AssistedInject constructor( } } + private fun getPayoutAddress(txData: TransactionData.Uncompiled): String { + val ethereumCallData = (txData.extras as? EthereumTransactionExtras)?.callData + return if (ethereumCallData is EthereumYieldSupplySendCallData) { + ethereumCallData.destinationAddress + } else { + txData.destinationAddress + } + } + companion object { private const val INCREASE_GAS_LIMIT_FOR_DEX = 112 // 12% private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% From 8c4f39aab53472381d22924ad962dd379ea23688 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Dec 2025 15:16:40 +0300 Subject: [PATCH 06/11] Updated on 2026-08-14 --- .../com/tangem/domain/apptheme/model/AppThemeMode.kt | 10 +++++----- features/wallet/impl/build.gradle.kts | 2 ++ .../feature/wallet/child/wallet/model/WalletModel.kt | 11 +++++++++-- .../wallet/analytics/WalletScreenAnalyticsEvent.kt | 9 ++++++++- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt b/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt index dd24287e13..85da14ca89 100644 --- a/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt +++ b/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt @@ -3,21 +3,21 @@ package com.tangem.domain.apptheme.model /** * Enumerates the possible modes for the application's theme. */ -enum class AppThemeMode { +enum class AppThemeMode(val value: String) { /** * Forces the dark theme mode regardless of system settings. */ - FORCE_DARK, + FORCE_DARK("Dark"), /** * Forces the light theme mode regardless of system settings. */ - FORCE_LIGHT, + FORCE_LIGHT("Light"), /** * Follows the system-wide theme mode. */ - FOLLOW_SYSTEM, + FOLLOW_SYSTEM("System"), ; @@ -30,6 +30,6 @@ enum class AppThemeMode { /** * List of available [AppThemeMode]s. * */ - val available: List = values().toList() + val available: List = entries } } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index ca7ea45cc6..7949e74246 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -103,6 +103,8 @@ dependencies { implementation(projects.domain.transaction) implementation(projects.domain.yieldSupply) implementation(projects.domain.yieldSupply.models) + implementation(projects.domain.appTheme) + implementation(projects.domain.appTheme.models) /** Feature Apis */ implementation(projects.features.details.api) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 2338f56503..4f4b296a7d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -10,6 +10,8 @@ import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.apptheme.GetAppThemeModeUseCase +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet @@ -96,6 +98,7 @@ internal class WalletModel @Inject constructor( private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val accountsFeatureToggles: AccountsFeatureToggles, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, + private val getAppThemeModeUseCase: GetAppThemeModeUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -106,14 +109,18 @@ internal class WalletModel @Inject constructor( private val walletsUpdateJobHolder = JobHolder() private val refreshWalletJobHolder = JobHolder() - private var needToRefreshWallet = false private val clearNFTCacheJobHolder = JobHolder() private val updateTangemPayJobHolder = JobHolder() + private var needToRefreshWallet = false private var expressTxStatusTaskScheduler = SingleTaskScheduler() init { - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) + modelScope.launch { + val result = getAppThemeModeUseCase().firstOrNull() + val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened(theme.value)) + } screenLifecycleProvider.isBackgroundState .onEach { isBackground -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index 54d3a4cca2..36cca3a129 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -51,7 +51,14 @@ sealed class WalletScreenAnalyticsEvent { params: Map = mapOf(), ) : AnalyticsEvent(category = "Main Screen", event = event, params = params) { - data object ScreenOpened : MainScreen(event = "Screen opened") + class ScreenOpened( + val theme: String, + ) : MainScreen( + event = "Screen opened", + params = mapOf( + "App Theme" to theme, + ), + ) class WalletSelected(val isImported: Boolean) : MainScreen( event = "Wallet Selected", From aa11b11fee8dcd6078c9cc663ddef51f2d61a8d6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Dec 2025 21:24:20 +0400 Subject: [PATCH 07/11] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 5 +- core/res/src/main/res/values-es/strings.xml | 9 ++- core/res/src/main/res/values-fr/strings.xml | 8 ++- core/res/src/main/res/values-it/strings.xml | 8 ++- core/res/src/main/res/values-ja/strings.xml | 49 +++++++------ core/res/src/main/res/values-ru/strings.xml | 17 +++-- .../src/main/res/values-uk-rUA/strings.xml | 1 + .../src/main/res/values-zh-rTW/strings.xml | 4 +- core/res/src/main/res/values/strings.xml | 71 ++++++++++--------- 9 files changed, 101 insertions(+), 71 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 0dc6af1380..af3e4fd8d7 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -392,6 +392,7 @@ Woche mit Ja + Ertragsmodus Vertragsadresse kopiert! Verfügbare Netzwerke Die Ableitung Deines Tokens entspricht der Ableitung von %1$s. Dein Token wird diesem Konto gutgeschrieben. @@ -1483,11 +1484,11 @@ Ein separates Zahlungskonto wird erstellt, ohne Ihre Adressen und Vermögenswerte offenzulegen Unerreichte Privatsphäre Erhalten Sie Ihre kostenlose Tangem Pay Card in wenigen Minuten - Payment account + Zahlungskonto Synchronisierung des Zahlungskontos erforderlich Wir beheben ein technisches Problem. Bitte versuchen Sie es später erneut. Service vorübergehend nicht verfügbar - Der Dienst ist derzeit nicht erreichbar. Bitte versuchen Sie es später erneut. + Daten können derzeit nicht angezeigt werden, Kartenzahlungen funktionieren jedoch weiterhin. Synchronisation erforderlich Tangem Visa Card Tangem Pay ist vorübergehend nicht verfügbar diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index b0aabad52d..e99920eeb5 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -345,6 +345,7 @@ semana con + Modo de rendimiento Dirección de contrato copiada Redes disponibles Dirección de contrato @@ -1319,11 +1320,11 @@ Se creará una cuenta de pago separada sin divulgar tus direcciones y activos Privacidad inigualable Obtén tu tarjeta Tangem Pay gratuita en minutos - Payment account + Cuenta de pago Sincronización de cuenta de pago necesaria Estamos solucionando un problema técnico. Por favor, inténtelo de nuevo más tarde. Servicio temporalmente no disponible - El servicio no está disponible actualmente. Inténtalo de nuevo más tarde. + No es posible mostrar los datos en este momento, pero los pagos con tarjeta siguen funcionando. Sincronización necesaria Tangem Visa Card Tangem Pay temporalmente no disponible @@ -1417,6 +1418,10 @@ Renombrar la billetera Desbloquear todo Desbloquear todo con %s + Configure un código de 4 dígitos.\nSe utilizará para los pagos. + Crear código PIN + No se aceptó el PIN. Inténtalo de nuevo o usa un código diferente. + PIN no válido: evitar secuencias o repeticiones Elija cómo agregar su billetera La blockchain está Inaccesible. Inténtelo más tarde Escanee la tarjeta o el anillo diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index ca122acb3d..faddf299c6 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -328,6 +328,7 @@ semaine avec Oui + Mode Rendement Adresse du contrat copiée ! Réseaux disponibles Adresse du contrat @@ -1300,11 +1301,11 @@ Un compte de paiement séparé sera créé sans divulguer vos adresses et actifs Confidentialité inégalée Obtenez votre carte Tangem Pay gratuite en quelques minutes - Payment account + Compte de paiement Synchronisation du compte de paiement nécessaire Nous réparons un problème technique. Veuillez réessayer plus tard. Service temporairement indisponible - Le service est actuellement indisponible. Veuillez réessayer plus tard. + Les données ne peuvent pas être affichées pour le moment, mais les paiements par carte fonctionnent toujours. Synchronisation requise Tangem Visa Card Tangem Pay est temporairement indisponible @@ -1408,6 +1409,9 @@ Cela ne prendra pas longtemps. Nous configurons votre compte. Cela ne prendra pas longtemps. Nous terminons l\'activation. Tout est en cours de préparation ! + Créez un code à 4 chiffres. Il servira pour les paiements. + Créer un code PIN + Le code PIN n\'a pas été accepté. Veuillez réessayer ou utiliser un autre code. Code PIN invalide : évitez les séquences ou les répétitions Accéder le site Web Continuons la configuration de votre compte. diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 179f7018a0..b7536b13ca 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -162,11 +162,11 @@ Verrà creato un conto di pagamento separato senza divulgare i tuoi indirizzi e asset Privacy senza rivali Ottieni la tua carta Tangem Pay gratuita in pochi minuti - Payment account + Conto di pagamento Sincronizzazione del conto di pagamento necessaria Stiamo risolvendo un problema tecnico. Riprova più tardi. Servizio temporaneamente non disponibile - Il servizio è attualmente non raggiungibile. Riprova più tardi. + Al momento non è possibile visualizzare i dati, ma i pagamenti con carta continuano a funzionare. Sincronizzazione necessaria Tangem Visa Card Tangem Pay è temporaneamente non disponibile @@ -174,6 +174,10 @@ Usa la tua carta o il tuo anello per ripristinare l\'accesso al tuo conto di pagamento Il tuo codice PIN Tangem Twin + Imposta un codice di 4 cifre.\nVerrà utilizzato per i pagamenti. + Crea codice PIN + Il PIN non è stato accettato. Riprova o utilizza un codice diverso. + PIN non valido: evitare sequenze o ripetizioni WalletConnect L\'indirizzo è stato copiato con successo Nessuna connessione a Internet diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 82aca73289..bce005b585 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -4,7 +4,7 @@ とにかくスキップ アクセスコードが設定されていません コードを変更 - アクセスコードは、ウォレットのロック解除・資産へのアクセス保護に使用されます + アクセスコードは、ウォレットのロック解除およびウォレットへのアクセス保護に使用されます このまま使う このアクセスコードは簡単に推測される可能性があります アクセスコードを入力 @@ -96,7 +96,7 @@ %1$s ネットワークのトークンは、ファームウェアの制限により、このカードまたはリングではサポートされていません。 カードまたはリングのスキャンに問題がありますか? このカードはこのアプリでは使用できません。 - まずアクセスコードを設定してから、生体認証を有効にしてください + 生体認証を有効にするには、アクセスコードを設定してください %1$sを使用してウォレットのロックを解除し、取引の署名などの機密性の高い操作を承認してください。ハードウェアウォレットの場合は、署名にカードまたはリングが必要です。 デフォルト手数料 デフォルト手数料を有効にすると、取引手数料が自動的に設定され、送金時に手数料ページを表示する必要がなくなります。必要に応じて、いつでもこのページに戻ることができます。 @@ -136,7 +136,7 @@ リカバリーフレーズ これらの単語は誰にも教えないでください。Tangemがこれらの単語を尋ねることは、絶対にありません。以下の%s個の単語はウォレットのリカバリーフレーズです。デバイスを紛失した場合、これらの単語を使用してウォレットを復元できます。 これらの%s個の単語を番号順に書き留め、安全かつ他人の目に触れない場所に保管してください - ウォレットおよびリカバリーフレーズの保護とバックアップは、すべてあなた自身の責任となります。 + ウォレットの安全確保およびリカバリーフレーズの安全なバックアップは、すべてご自身の責任となります。 リカバリーフレーズ 残高を表示または非表示にするには、デバイスの画面を下向きにするか、設定でオフにしてください。 今後表示しない @@ -386,6 +386,7 @@ はい + 利息モード コントラクトアドレスをコピーしました! 利用可能なネットワーク このトークンの導出パスが%1$sの導出パスと一致しています。トークンはこのアカウントに追加されます。 @@ -575,8 +576,8 @@ 今すぐ確定 ウォレットの設定を完了する アクセスコードでアプリを保護して、設定を完了してください。 - そうした場合は、最初からやり直す必要があります。 - 本当にアクティベーション処理を終了してもよろしいですか? + 終了すると、最初からやり直す必要があります。 + セットアップを中止してもよろしいですか? 暗号資産をオフラインで厳重保管。カードサイズで、金庫以上の安心を。 実行すると、最初からやり直す必要があります。 Googleドライブのバックアップから既存のウォレットを復元する @@ -585,18 +586,19 @@ 新しいウォレットを作成 Tangemの高性能ハードウェアウォレットで、セキュリティをさらに強化しましょう。 ハードウェアウォレット - 現在のウォレットをTangemウォレットに移します。 + 現在のウォレットをTangemに移します。 現在のウォレットをアップグレードする バックアップへ移動 アクセスコードを作成する前にウォレットをバックアップしてください。 - まずバックアップを完了する + 先にバックアップを完了してください まずバックアップを完了する その他の方法 - リカバリーフレーズは、ご自身で安全な場所に保管し、資金を守るために他人には絶対に共有しないでください。 + 資金を保護するため、リカバリーフレーズは安全な場所に保管し、他人に知られないようにしてください。 リカバリーフレーズ - アクセスコードを使用してウォレットを保護するには、まずバックアップを完了してください。 - ウォレットをハードウェアにアップグレードするには、まずバックアップしてください。 - 鍵はアプリに保存されます + アクセスコードでウォレットを保護するには、バックアップの手続きを完了してください。 + ハードウェアウォレットにアップグレードするには、バックアップの手続きを完了してください。 + 秘密鍵はデバイス上に保持されます + リカバリーフレーズを使ってウォレットを作成または復元してください。 シードフレーズのバックアップ モバイルウォレットを作成する 既存のウォレットをインポートする @@ -610,25 +612,25 @@ バックアップを表示 ウォレット削除 とにかく削除する - このウォレットにはバックアップが存在します。後で復元できるように、削除する前に内容を確認してください。 - バックアップなしでこのウォレットを削除すると、資金へのアクセスを永久に失います。 - このウォレットを削除しますか? - ウォレットの削除前にバックアップをしていなければ、アクセスを失う可能性があることを理解しています。 - ウォレットを削除しても中身自体が消えるわけではなく、単にこのデバイスから表示が消えるだけであることを理解しています。 - もはや、シードフレーズは不要。Tangemカードやリングが、そのまま安全なバックアップになります。 + このウォレットにはバックアップがあります。ウォレットを削除する前に、復元できることを確認してください。 + バックアップを行わずにこのウォレットを削除すると、資金へのアクセスを永久に失います。 + このウォレットを削除してもよろしいですか? + ウォレットを削除する前にバックアップを行っていない場合、ウォレットへのアクセスを失うことを理解しています。 + ウォレットを削除しても中身自体が消えるわけではなく、このデバイスから表示が消えるだけであることを理解しています。 + シードフレーズは不要です。Tangemカードまたはリングが安全なバックアップとなります。 Tangemでバックアップ アップグレードできません。このデバイスにはすでにウォレットが存在します。 別のデバイスを選択してください。このデバイスはアップグレードに使用できません。 操作中にエラーが発生しました。 処理中も、資金は安全に保たれ、常にアクセス可能です。 資金へのアクセス - ウォレットのデータはアプリから削除され、Tangemデバイスに保存されます。 + ウォレットのデータはアプリから削除され、ハードウェアウォレットに保存されます セキュリティ全般 - 秘密鍵はアプリからTangemデバイスに移動されます。 - 鍵の移行 + 秘密鍵はアプリからTangemハードウェアウォレットへ移行されます + キーの移行 デバイスをスキャン アップグレードを開始 - ハードウェアウォレットにアップグレードすると、資産がコールドストレージで安全に保管されます。 + ハードウェアウォレットへのアップグレードを行います。資産はコールドストレージで安全に保管されます。 Tangemウォレット ハードウェアウォレットにアップグレード Tangemの業界最高クラスのハードウェアウォレットで、暗号資産を安全に保管しましょう。 @@ -714,6 +716,7 @@ 上昇率上位 下落率上位 トレンド + 利息モード ステーキングは暗号資産で報酬を受け取る最も簡単な方法です。 %s 最大%s APYを獲得 トークンを追加しました @@ -963,6 +966,7 @@ その他の通貨 人気の法定通貨 通貨で検索 + この取引はすでに処理されています。これ以上の対応は必要ありません。 最良のレートを取得しています... 即時 オンランプ機能を使用することにより、プロバイダの%1$sおよび%2$sに同意するものとします @@ -1481,7 +1485,7 @@ 支払いアカウントの同期が必要です 技術的な問題を修正しています。後でもう一度お試しください。 サービスは一時的に利用できません - 現在サービスに接続できません。後ほどもう一度お試しください。 + 現在、データを表示できませんが、カードでのお支払いは引き続きご利用いただけます。 同期が必要です Tangem Visaカード Tangem Payは一時的に利用できません @@ -1589,6 +1593,7 @@ 長くはかかりません。アクティベーションを完了しています。 準備完了です! 4桁のコードを設定します。 \nお支払いの際に使用されます。 + PINコードを作成する PINの認証に失敗しました。もう一度お試しいただくか、別のコードを使用してください。 無効な暗証番号:連続や繰り返しを避けてください ウェブサイトに移動 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 4b22469076..0d5eaa197f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -402,6 +402,7 @@ неделю с Да + Режим доходности Адрес контракта скопирован! Доступные сети Деривация вашего токена совпадает с деривацией %1$s. Токен будет добавлен к этой учётной записи. @@ -1450,7 +1451,7 @@ Разморозить карту? Не удалось разморозить карту, попробуйте еще раз Карта разморожена - Вывод + Вывести Пополнить Способы пополнения Номер @@ -1486,7 +1487,7 @@ Пополните карту любым активом через обмен Реквизиты Разморозить карту - Вывод + Вывести Вывод сейчас недоступен Вы не можете начать обмен или новый вывод, пока не завершится текущий. Вывод выполняется @@ -1518,16 +1519,16 @@ Мы создадим отдельный платежный счет без раскрытия ваших активов \nи их адресов Абсолютная приватность Откройте виртуальную \nTangem Pay Card - Payment account - Требуется синхронизация платежного счета + Платежный аккаунт + Требуется синхронизация платежного аккаунта Мы устраняем техническую проблему. Пожалуйста, попробуйте позже. Сервис временно недоступен - Сервис временно недоступен. Пожалуйста, попробуйте позже. + Не можем показать данные карты, но оплаты продолжают работать. Требуется синхронизация Tangem Visa Card Tangem Pay временно недоступен Tangem Pay - Используйте вашу карту или кольцо для восстановления доступа к платежному счету + Используйте вашу карту или кольцо для восстановления доступа к платежному аккаунту Ваш PIN-код Это мой кошелек Балансы скрыты @@ -1621,7 +1622,11 @@ Разблокировать все с %s Код доступа Активация аккаунта + Установите 4-значный код.\nОн будет использоваться для платежей. + ПИН-код + Установить ПИН-код ПИН не принят. Попробуйте ещё раз или введите другой код. + Слабый ПИН: не используйте повторы или последовательности. Разблокировать Выберите способ добавления кошелька Отсканируйте вашу карту или кольцо Tangem, чтобы восстановить её или импортировать из другого кошелька. 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 98ddcc642a..92eff0a832 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -257,6 +257,7 @@ тиждень з Так + Режим дохідності Адреса контракту скопійована! Доступні мережі Адреса контракту 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 b2f04957dd..6fa2c8b69c 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -405,11 +405,11 @@ 將創建單獨的支付帳戶,且不會透露您的地址和資產 無與倫比的隱私 在幾分鐘內獲得免費的 Tangem Pay 卡 - Payment account + 付款帳戶 需要同步支付账户 我们正在修复技术问题。请稍后再试。 服務暫時無法使用 - 服务当前不可用,请稍后再试 + 目前無法顯示資料,但卡片支付仍可正常使用。 需要同步 Tangem Visa Card Tangem Pay暂时不可用 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8c52e119ff..d365c6e247 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -4,7 +4,7 @@ Skip anyway Access code not set Change code - Your access code will be used to unlock your wallet and to protect access to the assets + Your access code unlocks and protects access to your wallet Use anyway This access code can be easily guessed Enter access code @@ -96,7 +96,7 @@ Tokens in %1$s network are not supported by this card or ring due to firmware limitation. Are you having difficulty scanning your card or ring? This card is not designed to work with this app - Set an access code first to enable biometrics + Set an access code to enable biometrics Use %1$s to unlock your wallet and approve sensitive actions, like signing transactions. For hardware wallets, a card or ring is still required to sign. Default Fee Enable Default Fee to set transaction fees automatically and skip the Fee page when sending funds. You can always go back to this page if necessary. @@ -137,7 +137,7 @@ Recovery phrase Never share these words with anyone. Tangem will never ask you for them. The %s words below are your wallet\'s recovery phrase. Use them to restore your wallet if you lose your device. Write down these %s words in numerical order and keep them safe and private - You are fully responsible for securing and backing up your wallet and recovery phrase. + You are fully responsible for securing your wallet and safely backing up your recovery phrase. Recovery phrase To hide or show your balances, simply flip your device screen down, or switch it off in Settings Don\'t show again @@ -394,6 +394,7 @@ week with Yes + Yield Mode Contract address copied! Available networks Derivation of your token matches the derivation of %1$s. Your token will be added to this account. @@ -583,31 +584,31 @@ Finalize now Finalize wallet setup Complete setup by securing the app with an access code. - If you do, you\'ll need to start over. - Are you sure you want to quit the activation process? - Keeps your crypto safe and offline. Slim as a credit card, safer than a bank vault. + If you exit, you\'ll need to start over. + Are you sure you want to quit the setup process? + Keeps your crypto safe and offline. Slim as a credit card, safer than a bank vault. If you do, you\'ll need to start over. Recover existing wallet via Google Drive backup Google Drive backup - Create a new secure wallet and transfer your funds for extra protection. + Create a secure wallet and transfer your funds for extra protection. Create new wallet Level up your security with the superior Tangem hardware wallet. - Hardware Wallet - Move your current wallet into Tangem Wallet. + Hardware wallet + Move your current wallet into Tangem. Upgrade current wallet Go to backup Please back up your wallet before creating an access code. - Finish Backup First + Finish backup first Finalize backup first Incomplete Other methods - Manually save your recovery phrase in a secure place and keep it private to protect your funds. + Save your recovery phrase in a secure place and keep it private to protect your funds. Recovery phrase - To secure your wallet with an Access Code, complete the backup first. - To upgrade your wallet to hardware, back it up first. + To secure your wallet with an access code, complete the backup process. + To upgrade to a hardware wallet, complete the backup process. Your private keys are securely encrypted and stored on your phone - Keys are stored in the app - Create or restore your wallet using a recovery phrase — your built-in backup. + Private keys stay on your device + Create or restore your wallet with a recovery phrase. Seed phrase backup Create Mobile Wallet Import existing wallet @@ -620,28 +621,28 @@ Go to backup View backup Forget wallet - Forget Anyway - A backup for this wallet exists. Review it before forgetting to make sure you can recover later. - If you forget this wallet without a backup, you’ll permanently lose access to your funds. - Forget this wallet? - I understand that if I haven\'t backed up my wallet before removing it, I may lose access to it. - I understand that removing my wallet does not delete it—but simply removes it from my device. - No seed phrase is needed anymore. Your Tangem card or ring becomes your secure backup. + Forget anyway + This wallet has a backup. Ensure you can recover it before forgetting the wallet. + If you forget this wallet without a backup, you\'ll permanently lose access to your funds. + Are you sure you want to forget this wallet? + I understand that if I haven\'t backed up my wallet before removing it, I will lose access to it. + I understand that removing my wallet does not delete it, only removes it from my device. + Seed phrase not required. Your Tangem card or ring becomes your secure backup. Backup with Tangem - Can’t upgrade. A wallet already exists on this device. + Can\'t upgrade. A wallet already exists on this device. Pick another device. This one can’t be used for the upgrade. An error occurred during the operation. Your funds remain safe and fully accessible during the process Access to funds - Your wallet data will be erased from the app and stored on your Tangem device + Your wallet data will be erased from the app and stored on your hardware wallet General security - Private keys will be moved from the app to your Tangem device - Key Migration + Private keys will be moved from the app to your Tangem hardware wallet + Key migration Scan device Start upgrade - You’re about to upgrade to our hardware wallet. This will keep your assets safe in cold storage. + You\'re about to upgrade to our hardware wallet. It will keep your assets safe in cold storage. Tangem Wallet - Upgrade to Hardware Wallet + Upgrade to our Hardware Wallet Keep your crypto safe with Tangem\'s top-tier hardware wallet. Upgrade your wallet to hardware security This information was generated with AI.\nTap here, if you find any errors. @@ -726,6 +727,7 @@ Top Gainers Top Losers Trending + Yield Mode Staking is the easiest way to receive rewards on your crypto. %s Earn up to %s APY Token Added @@ -801,7 +803,7 @@ Mobile Wallet requires %1$s or later All news - + %dh ago %dh ago @@ -983,6 +985,7 @@ Other currencies Popular Fiats Search by currency + This transaction has already been processed. No further action is required. Fetching best rates... Instant By using onramp functionality, you agree with provider’s %1$s and %2$s @@ -1435,6 +1438,7 @@ Failed to unfreeze the card. Try again later. Your card is unfrozen. Withdrawal + Unable to use on rooted device Add funds Top-up options Card Number @@ -1476,6 +1480,7 @@ Withdrawal in progress Change PIN-code Come back to the app if you forget it. + I understand that I will completely lose access to my Tangem Pay Card and all funds on it without the possibility of recovery Failed to issue card A technical error has occurred, please try again by clicking the button below. A technical error has occurred, please contact support. @@ -1506,10 +1511,10 @@ Payment account sync needed We’re fixing a technical issue. Please try again later. Service temporarily unavailable - The service is currently unreachable. Please try again later. + Unable to display details. However, card payments are still working. Sync needed Tangem Visa Card - Tangem Pay is temporarily unavailable + Tangem Pay is temporarily unreachable Tangem Pay Use your card or ring to restore access to your payment account Your PIN code @@ -1727,7 +1732,7 @@ Using a Tangem Wallet already? Scan now Pick a wallet setup method - Want to purchase Tangem Wallet? + Want to buy a Tangem Wallet? Buy now Recover existing wallet via Google Drive backup Import from Google Drive @@ -1957,7 +1962,7 @@ Create or import a software wallet on your phone. Start with Mobile Wallet Other method - Use Tangem Hardware Wallet + Use a Tangem hardware wallet Learn more & buy Discard You have an interrupted backup. Do you want to resume? From 1e8890dac42b25aafa3b6d4eb1df79a47ccd1f0b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 12:59:21 +0400 Subject: [PATCH 08/11] Updated on 2026-08-14 --- .../YieldSupplyGetRewardsBalanceUseCase.kt | 3 ++ ...YieldSupplyGetRewardsBalanceUseCaseTest.kt | 32 +++++++++++++++++++ .../TokenDetailsLoadedBalanceConverter.kt | 12 +++++-- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt index 93e18928bf..f0b5c39b43 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt @@ -29,6 +29,9 @@ class YieldSupplyGetRewardsBalanceUseCase( val fiatRate = status.value.fiatRate if (cryptoAmount?.compareTo(BigDecimal.ZERO) == 0) { + emit( + YieldSupplyRewardBalance(fiatBalance = null, cryptoBalance = null), + ) return@flow } diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt index c086813f73..d8b29bfd94 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt @@ -49,6 +49,38 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { assertThat(emissions).isEmpty() } + @Test + fun `GIVEN zero amount WHEN invoke THEN emit null balances`() = runTest { + val network = createNetwork() + val token = createToken(network) + val status = CryptoCurrencyStatus( + currency = token, + value = CryptoCurrencyStatus.Custom( + amount = BigDecimal.ZERO, + fiatAmount = null, + fiatRate = BigDecimal.ONE, + priceChange = null, + yieldBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = NetworkAddress.Single( + NetworkAddress.Address(value = "0xabc", type = NetworkAddress.Address.Type.Primary), + ), + sources = CryptoCurrencyStatus.Sources(), + ), + ) + + val dispatcherProvider = testDispatcherProvider(this) + val useCase = YieldSupplyGetRewardsBalanceUseCase(repository, dispatcherProvider) + val appCurrency = AppCurrency.Default + + val emissions = useCase(status, appCurrency).toList() + assertThat(emissions).hasSize(1) + assertThat(emissions[0].fiatBalance).isNull() + assertThat(emissions[0].cryptoBalance).isNull() + } + @Test fun `GIVEN coin currency WHEN invoke THEN emit nothing`() = runTest { val network = createNetwork() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 306315e766..908fdf6c7e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -98,8 +98,16 @@ internal class TokenDetailsLoadedBalanceConverter( stakingCryptoAmount, currentState.selectedBalanceType, ), - displayYieldSupplyFiatBalance = (currentState as? TokenDetailsBalanceBlockState.Content) - ?.displayYieldSupplyFiatBalance, + displayYieldSupplyFiatBalance = if (status.value.yieldSupplyStatus?.isActive == true) { + (currentState as? TokenDetailsBalanceBlockState.Content)?.displayYieldSupplyFiatBalance + } else { + null + }, + displayYieldSupplyCryptoBalance = if (status.value.yieldSupplyStatus?.isActive == true) { + (currentState as? TokenDetailsBalanceBlockState.Content)?.displayYieldSupplyCryptoBalance + } else { + null + }, balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, onBalanceSelect = clickIntents::onBalanceSelect, selectedBalanceType = currentState.selectedBalanceType, From 0e85eb1d78c70868e1da41cdaa9d3f279f9713e1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 13:19:18 +0300 Subject: [PATCH 09/11] Updated on 2026-08-14 --- .../feature/swap/analytics/SwapEvents.kt | 28 ++- .../tangem/feature/swap/model/SwapModel.kt | 187 ++++++++++++------ 2 files changed, 155 insertions(+), 60 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 8d8ed4d072..96e04a99e6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -43,18 +43,31 @@ sealed class SwapEvents( params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken), ) - data object ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission") + data class ButtonGivePermissionClicked( + val sendToken: String, + val receiveToken: String, + val provider: SwapProvider, + ) : SwapEvents( + event = "Button - Give permission", + params = mapOf( + "Send Token" to sendToken, + "Receive Token" to receiveToken, + "Provider" to provider.name, + ), + ) data class ButtonPermissionApproveClicked( val sendToken: String, val receiveToken: String, val approveType: ApproveType, + val provider: SwapProvider, ) : SwapEvents( event = "Button - Permission Approve", params = mapOf( "Send Token" to sendToken, "Receive Token" to receiveToken, "Type" to if (approveType == ApproveType.LIMITED) "Current Transaction" else "Unlimited", + "Provider" to provider.name, ), ) @@ -141,4 +154,17 @@ sealed class SwapEvents( data object Recommended : PromoState("Recommended") } } + + data class NoticePermissionNeeded( + val sendToken: String, + val receiveToken: String, + val provider: SwapProvider, + ) : SwapEvents( + event = "Notice - Permission Needed", + params = mapOf( + "Send Token" to sendToken, + "Receive Token" to receiveToken, + "Provider" to provider.name, + ), + ) } \ No newline at end of file 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 d227b21dfd..649b97059f 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 @@ -189,6 +189,7 @@ internal class SwapModel @Inject constructor( private val toTokenBalanceJobHolder = JobHolder() private var isAmountChangedByUser: Boolean = false + private var lastPermissionNotificationTokens: Pair? = null val currentScreen: SwapNavScreen get() = swapRouter.currentScreen @@ -544,60 +545,101 @@ internal class SwapModel @Inject constructor( private fun setupLoadedState(provider: SwapProvider, state: SwapState, fromToken: CryptoCurrencyStatus) { when (state) { is SwapState.QuotesLoadedState -> { - fillLoadedDataState(state, state.permissionState, state.swapDataModel) - val loadedStates = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() - val bestRatedProviderId = findBestQuoteProvider(loadedStates)?.providerId ?: provider.providerId - uiState = stateBuilder.createQuotesLoadedState( - uiStateHolder = uiState, - quoteModel = state, - fromToken = fromToken.currency, - feeCryptoCurrencyStatus = dataState.feePaidCryptoCurrency, - swapProvider = provider, - bestRatedProviderId = bestRatedProviderId, - isNeedBestRateBadge = dataState.lastLoadedSwapStates.consideredProvidersStates().size > 1, - selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, - isReverseSwapPossible = isReverseSwapPossible(), - needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), - hideFee = tangemPayInput?.isWithdrawal == true, - ) - if (uiState.notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }) { - analyticsEventHandler.send( - SwapEvents.NoticeNotEnoughFee( - token = initialCurrencyFrom.symbol, - blockchain = fromToken.currency.network.name, - ), - ) - } + setupQuotesLoadedUiState(provider, state, fromToken) + sendAnalyticsForNotifications(fromToken) + updatePermissionNotificationState(state) } is SwapState.EmptyAmountState -> { - val toTokenStatus = dataState.toCryptoCurrency - uiState = stateBuilder.createQuotesEmptyAmountState( - uiStateHolder = uiState, - emptyAmountState = state, - fromTokenStatus = fromToken, - toTokenStatus = toTokenStatus, - isReverseSwapPossible = isReverseSwapPossible(), - toAccount = dataState.toAccount, - ) + setupEmptyAmountUiState(state, fromToken) + lastPermissionNotificationTokens = null } is SwapState.SwapError -> { - singleTaskScheduler.cancelTask() - uiState = stateBuilder.createQuotesErrorState( - uiStateHolder = uiState, - swapProvider = provider, - fromToken = state.fromTokenInfo, - toToken = dataState.toCryptoCurrency, - expressDataError = state.error, - includeFeeInAmount = state.includeFeeInAmount, - isReverseSwapPossible = isReverseSwapPossible(), - needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), - toAccount = dataState.toAccount, - ) - sendErrorAnalyticsEvent(state.error, provider) + setupErrorUiState(provider, state) + lastPermissionNotificationTokens = null } } } + private fun setupQuotesLoadedUiState( + provider: SwapProvider, + state: SwapState.QuotesLoadedState, + fromToken: CryptoCurrencyStatus, + ) { + fillLoadedDataState(state, state.permissionState, state.swapDataModel) + val loadedStates = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() + val bestRatedProviderId = findBestQuoteProvider(loadedStates)?.providerId ?: provider.providerId + uiState = stateBuilder.createQuotesLoadedState( + uiStateHolder = uiState, + quoteModel = state, + fromToken = fromToken.currency, + feeCryptoCurrencyStatus = dataState.feePaidCryptoCurrency, + swapProvider = provider, + bestRatedProviderId = bestRatedProviderId, + isNeedBestRateBadge = dataState.lastLoadedSwapStates.consideredProvidersStates().size > 1, + selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, + isReverseSwapPossible = isReverseSwapPossible(), + needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), + hideFee = tangemPayInput?.isWithdrawal == true, + ) + } + + private fun sendAnalyticsForNotifications(fromToken: CryptoCurrencyStatus) { + if (uiState.notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }) { + analyticsEventHandler.send( + SwapEvents.NoticeNotEnoughFee( + token = initialCurrencyFrom.symbol, + blockchain = fromToken.currency.network.name, + ), + ) + } + } + + private fun updatePermissionNotificationState(state: SwapState.QuotesLoadedState) { + val fromTokenId = state.fromTokenInfo.cryptoCurrencyStatus + .currency.id.value + val toTokenId = state.toTokenInfo.cryptoCurrencyStatus + .currency.id.value + val currentTokenPair = Pair(fromTokenId, toTokenId) + + when { + uiState.notifications.none { it is SwapNotificationUM.Info.PermissionNeeded } -> { + lastPermissionNotificationTokens = null + } + lastPermissionNotificationTokens != currentTokenPair -> { + sendNoticePermissionNeededEvent() + lastPermissionNotificationTokens = currentTokenPair + } + } + } + + private fun setupEmptyAmountUiState(state: SwapState.EmptyAmountState, fromToken: CryptoCurrencyStatus) { + val toTokenStatus = dataState.toCryptoCurrency + uiState = stateBuilder.createQuotesEmptyAmountState( + uiStateHolder = uiState, + emptyAmountState = state, + fromTokenStatus = fromToken, + toTokenStatus = toTokenStatus, + isReverseSwapPossible = isReverseSwapPossible(), + toAccount = dataState.toAccount, + ) + } + + private fun setupErrorUiState(provider: SwapProvider, state: SwapState.SwapError) { + singleTaskScheduler.cancelTask() + uiState = stateBuilder.createQuotesErrorState( + uiStateHolder = uiState, + swapProvider = provider, + fromToken = state.fromTokenInfo, + toToken = dataState.toCryptoCurrency, + expressDataError = state.error, + includeFeeInAmount = state.includeFeeInAmount, + isReverseSwapPossible = isReverseSwapPossible(), + needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), + toAccount = dataState.toAccount, + ) + sendErrorAnalyticsEvent(state.error, provider) + } + private fun sendErrorAnalyticsEvent(error: ExpressDataError, provider: SwapProvider) { val receiveToken = dataState.toCryptoCurrency?.currency?.let { "${it.network.backendId}:${it.symbol}" @@ -1331,7 +1373,7 @@ internal class SwapModel @Inject constructor( onReduceByAmount = ::onReduceAmountClicked, openPermissionBottomSheet = { singleTaskScheduler.cancelTask() - analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) + sendGivePermissionClickedEvent() uiState = stateBuilder.showPermissionBottomSheet(uiState) { startLoadingQuotesFromLastState(isSilent = true) analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked) @@ -1579,19 +1621,46 @@ internal class SwapModel @Inject constructor( }.map { it.currency }.contains(chosen.currency) } + private fun sendNoticePermissionNeededEvent() { + val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol ?: return + val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return + val provider = dataState.selectedProvider ?: return + analyticsEventHandler.send( + SwapEvents.NoticePermissionNeeded( + sendToken = sendTokenSymbol, + receiveToken = receiveTokenSymbol, + provider = provider, + ), + ) + } + + private fun sendGivePermissionClickedEvent() { + val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol ?: return + val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return + val provider = dataState.selectedProvider ?: return + analyticsEventHandler.send( + SwapEvents.ButtonGivePermissionClicked( + sendToken = sendTokenSymbol, + receiveToken = receiveTokenSymbol, + provider = provider, + ), + ) + } + private fun sendPermissionApproveClickedEvent() { - val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol - val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol - val approveType = uiState.permissionState.getApproveTypeOrNull() - if (sendTokenSymbol != null && receiveTokenSymbol != null && approveType != null) { - analyticsEventHandler.send( - SwapEvents.ButtonPermissionApproveClicked( - sendToken = sendTokenSymbol, - receiveToken = receiveTokenSymbol, - approveType = approveType, - ), - ) - } + val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol ?: return + val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return + val approveType = uiState.permissionState.getApproveTypeOrNull() ?: return + val provider = dataState.selectedProvider ?: return + + analyticsEventHandler.send( + SwapEvents.ButtonPermissionApproveClicked( + sendToken = sendTokenSymbol, + receiveToken = receiveTokenSymbol, + approveType = approveType, + provider = provider, + ), + ) } private fun updateWalletBalance() { From 929dae92c388cb1c472fc981894fe5152806413c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 13:51:47 +0300 Subject: [PATCH 10/11] Updated on 2026-08-14 --- .../tangem/domain/models/wallet/UserWallet.kt | 7 ++++++ .../wallet/child/wallet/model/WalletModel.kt | 24 ++++++++++++------- .../analytics/WalletScreenAnalyticsEvent.kt | 2 ++ .../utils/SelectedWalletAnalyticsSender.kt | 8 +------ 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt index 30b9500769..8f2b0572ff 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt @@ -86,6 +86,13 @@ fun UserWallet.requireHotWallet(): UserWallet.Hot { ?: error("This user wallet is not a hot wallet") } +fun UserWallet.isImported(): Boolean { + return when (this) { + is UserWallet.Cold -> isImported + is UserWallet.Hot -> true + } +} + fun UserWallet.copy(name: String = this.name, walletId: UserWalletId = this.walletId): UserWallet = when (this) { is UserWallet.Cold -> this.copy(name = name, walletId = walletId) is UserWallet.Hot -> this.copy(name = name, walletId = walletId) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 4f4b296a7d..43aac5ae23 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -16,6 +16,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase 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.models.wallet.isImported import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase @@ -116,12 +117,6 @@ internal class WalletModel @Inject constructor( private var expressTxStatusTaskScheduler = SingleTaskScheduler() init { - modelScope.launch { - val result = getAppThemeModeUseCase().firstOrNull() - val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened(theme.value)) - } - screenLifecycleProvider.isBackgroundState .onEach { isBackground -> if (isBackground.not()) { @@ -278,8 +273,8 @@ internal class WalletModel @Inject constructor( // It's okay here because we need to be able to observe the selected wallet changes @Suppress("DEPRECATION") private fun subscribeOnSelectedWalletFlow() { - getSelectedWalletUseCase().onRight { - it + getSelectedWalletUseCase().onRight { walletFlow -> + walletFlow .conflate() .distinctUntilChanged() .onEach { selectedWallet -> @@ -289,6 +284,8 @@ internal class WalletModel @Inject constructor( subscribeOnExpressTransactionsUpdates(selectedWallet) observeAndClearNFTCacheIfNeedUseCase(selectedWallet) + + sendMainScreenOpenedAnalytics(selectedWallet.isImported()) } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -698,6 +695,17 @@ internal class WalletModel @Inject constructor( } } + private suspend fun sendMainScreenOpenedAnalytics(isImported: Boolean) { + val result = getAppThemeModeUseCase().firstOrNull() + val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM + analyticsEventsHandler.send( + WalletScreenAnalyticsEvent.MainScreen.ScreenOpened( + theme = theme.value, + isImported = isImported, + ), + ) + } + inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks { override fun onAllowed() { analyticsEventsHandler.send(MainScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.On)) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index 36cca3a129..3ecfa644f8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -53,10 +53,12 @@ sealed class WalletScreenAnalyticsEvent { class ScreenOpened( val theme: String, + val isImported: Boolean, ) : MainScreen( event = "Screen opened", params = mapOf( "App Theme" to theme, + "Wallet Type" to if (isImported) "Seed Phrase" else "Seedless", ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt index 90e94e979a..ccb889e197 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isImported import com.tangem.domain.models.wallet.isLocked import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider @@ -32,11 +33,4 @@ internal class SelectedWalletAnalyticsSender @Inject constructor( else -> WalletScreenAnalyticsEvent.MainScreen.WalletSelected(userWallet.isImported()) } - - private fun UserWallet.isImported(): Boolean { - return when (this) { - is UserWallet.Cold -> isImported - is UserWallet.Hot -> true - } - } } \ No newline at end of file From 2e10db5bd558df3bba9935d0b2075e5b1387fee3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 10:52:06 +0000 Subject: [PATCH 11/11] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index d167ffb2eb..e5b0dbadd1 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.31.1-1326" +tangemBlockchainSdk = "develop-1327" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.31-569" +tangemCardSdk = "develop-573" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-531" +tangemHotSdk = "develop-532" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^