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 b7fc3cc35a..7272171df2 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 @@ -235,6 +235,11 @@ sealed class NotificationUM(val config: NotificationConfig) { subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)), ) + data object HighNetworkFee : Warning( + title = resourceReference(id = R.string.high_fee_warning_title), + subtitle = resourceReference(id = R.string.high_fee_warning_description), + ) + data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning( title = resourceReference(R.string.send_fee_unreachable_error_title), subtitle = resourceReference(R.string.send_fee_unreachable_error_text), 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 97f8587ac4..4504deb187 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 @@ -179,6 +179,10 @@ "name": "TWI_1469_FOR_YOU_ENABLED", "version": "undefined" }, + { + "name": "TWI_1367_HIGH_FEE_WARNING_ENABLED", + "version": "undefined" + }, { "name": "TWI_1638_VA_MVP0_ENABLED", "version": "6.1" diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt index ffd7ce636e..937d609175 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt @@ -15,6 +15,7 @@ import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.domain.quotes.GetCurrencyUSDQuoteUseCase +import com.tangem.domain.quotes.IsHighNetworkFeeUseCase import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteUpdater @@ -79,4 +80,12 @@ internal object QuotesDataModule { fun provideGetCurrencyUSDQuoteUseCase(quotesRepository: QuotesRepository): GetCurrencyUSDQuoteUseCase { return GetCurrencyUSDQuoteUseCase(quotesRepository) } + + @Singleton + @Provides + fun provideIsHighNetworkFeeUseCase( + getCurrencyUSDQuoteUseCase: GetCurrencyUSDQuoteUseCase, + ): IsHighNetworkFeeUseCase { + return IsHighNetworkFeeUseCase(getCurrencyUSDQuoteUseCase) + } } \ No newline at end of file diff --git a/domain/quotes/build.gradle.kts b/domain/quotes/build.gradle.kts index 27f51399d9..e6946d8787 100644 --- a/domain/quotes/build.gradle.kts +++ b/domain/quotes/build.gradle.kts @@ -6,4 +6,6 @@ plugins { dependencies { api(projects.domain.core) api(projects.domain.models) + + testImplementation(projects.test.core) } \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/IsHighNetworkFeeUseCase.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/IsHighNetworkFeeUseCase.kt new file mode 100644 index 0000000000..b37ff91417 --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/IsHighNetworkFeeUseCase.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.quotes + +import com.tangem.domain.models.currency.CryptoCurrency +import java.math.BigDecimal + +/** + * Checks whether a network fee is higher than a single hardcoded USD threshold, applied uniformly + * across all networks. The fee USD value is computed from the fee currency's USD quote + * ([GetCurrencyUSDQuoteUseCase]), independent of the user's selected app currency. + * + * Returns `false` when there is no USD quote or no raw currency id — never warn without pricing data. + */ +class IsHighNetworkFeeUseCase( + private val getCurrencyUSDQuoteUseCase: GetCurrencyUSDQuoteUseCase, +) { + + suspend operator fun invoke(feeCurrency: CryptoCurrency, feeAmount: BigDecimal): Boolean { + val rawCurrencyId = feeCurrency.id.rawCurrencyId ?: return false + val usdRate = getCurrencyUSDQuoteUseCase(rawCurrencyId) ?: return false + + return feeAmount.multiply(usdRate) > HIGH_FEE_USD_THRESHOLD + } + + private companion object { + val HIGH_FEE_USD_THRESHOLD = BigDecimal("10") + } +} \ No newline at end of file diff --git a/domain/quotes/src/test/kotlin/com/tangem/domain/quotes/IsHighNetworkFeeUseCaseTest.kt b/domain/quotes/src/test/kotlin/com/tangem/domain/quotes/IsHighNetworkFeeUseCaseTest.kt new file mode 100644 index 0000000000..72310339ef --- /dev/null +++ b/domain/quotes/src/test/kotlin/com/tangem/domain/quotes/IsHighNetworkFeeUseCaseTest.kt @@ -0,0 +1,87 @@ +package com.tangem.domain.quotes + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.currency.CryptoCurrency +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +internal class IsHighNetworkFeeUseCaseTest { + + private val getCurrencyUSDQuoteUseCase: GetCurrencyUSDQuoteUseCase = mockk() + private val feeCurrency: CryptoCurrency = mockk() + private val rawCurrencyId = CryptoCurrency.RawID("bitcoin") + + private val useCase = IsHighNetworkFeeUseCase(getCurrencyUSDQuoteUseCase) + + @BeforeEach + fun setup() { + clearMocks(getCurrencyUSDQuoteUseCase, feeCurrency) + every { feeCurrency.id.rawCurrencyId } returns rawCurrencyId + } + + @Test + fun `GIVEN fee usd value above threshold WHEN invoke THEN returns true`() = runTest { + // Arrange — 0.5 coin * 25 USD = 12.5 USD > 10 + coEvery { getCurrencyUSDQuoteUseCase(rawCurrencyId) } returns BigDecimal("25") + + // Act + val result = useCase(feeCurrency, BigDecimal("0.5")) + + // Assert + assertThat(result).isTrue() + } + + @Test + fun `GIVEN fee usd value below threshold WHEN invoke THEN returns false`() = runTest { + // Arrange — 0.2 coin * 25 USD = 5 USD < 10 + coEvery { getCurrencyUSDQuoteUseCase(rawCurrencyId) } returns BigDecimal("25") + + // Act + val result = useCase(feeCurrency, BigDecimal("0.2")) + + // Assert + assertThat(result).isFalse() + } + + @Test + fun `GIVEN fee usd value equal to threshold WHEN invoke THEN returns false`() = runTest { + // Arrange — 0.4 coin * 25 USD = 10 USD, not strictly above threshold + coEvery { getCurrencyUSDQuoteUseCase(rawCurrencyId) } returns BigDecimal("25") + + // Act + val result = useCase(feeCurrency, BigDecimal("0.4")) + + // Assert + assertThat(result).isFalse() + } + + @Test + fun `GIVEN no usd quote WHEN invoke THEN returns false`() = runTest { + // Arrange + coEvery { getCurrencyUSDQuoteUseCase(rawCurrencyId) } returns null + + // Act + val result = useCase(feeCurrency, BigDecimal("100")) + + // Assert + assertThat(result).isFalse() + } + + @Test + fun `GIVEN no raw currency id WHEN invoke THEN returns false`() = runTest { + // Arrange + every { feeCurrency.id.rawCurrencyId } returns null + + // Act + val result = useCase(feeCurrency, BigDecimal("100")) + + // Assert + assertThat(result).isFalse() + } +} \ No newline at end of file diff --git a/features/send/api/src/main/java/com/tangem/features/send/api/SendFeatureToggles.kt b/features/send/api/src/main/java/com/tangem/features/send/api/SendFeatureToggles.kt index 38ac439890..87b31ad6d7 100644 --- a/features/send/api/src/main/java/com/tangem/features/send/api/SendFeatureToggles.kt +++ b/features/send/api/src/main/java/com/tangem/features/send/api/SendFeatureToggles.kt @@ -1,3 +1,5 @@ package com.tangem.features.send.api -interface SendFeatureToggles \ No newline at end of file +interface SendFeatureToggles { + val isHighFeeWarningEnabled: Boolean +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/DefaultSendFeatureToggles.kt b/features/send/impl/src/main/java/com/tangem/features/send/DefaultSendFeatureToggles.kt index 6c5e2ee2bf..20a4eb7415 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/DefaultSendFeatureToggles.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/DefaultSendFeatureToggles.kt @@ -1,6 +1,16 @@ package com.tangem.features.send +import com.tangem.core.configtoggle.FeatureToggles +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.send.api.SendFeatureToggles import javax.inject.Inject -internal class DefaultSendFeatureToggles @Inject constructor() : SendFeatureToggles \ No newline at end of file +internal class DefaultSendFeatureToggles @Inject constructor( + private val featureTogglesManager: FeatureTogglesManager, +) : SendFeatureToggles { + + override val isHighFeeWarningEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled( + toggle = FeatureToggles.TWI_1367_HIGH_FEE_WARNING_ENABLED, + ) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/SendConfirmModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/SendConfirmModel.kt index bf529097bc..48dabef006 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/SendConfirmModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/SendConfirmModel.kt @@ -34,6 +34,7 @@ import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.isHotWallet +import com.tangem.domain.quotes.IsHighNetworkFeeUseCase import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase @@ -43,6 +44,7 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.utils.convertToSdkAmount +import com.tangem.features.send.api.SendFeatureToggles import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.api.subcomponents.amount.SendAmountReduceTrigger @@ -113,6 +115,8 @@ internal class SendConfirmModel @Inject constructor( private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val currenciesRepository: CurrenciesRepository, private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase, + private val isHighNetworkFeeUseCase: IsHighNetworkFeeUseCase, + private val sendFeatureToggles: SendFeatureToggles, sendBalanceUpdaterFactory: SendBalanceUpdater.Factory, ) : Model(), SendConfirmClickIntents, FeeSelectorModelCallback, SendNotificationsComponent.ModelCallback { @@ -503,6 +507,7 @@ internal class SendConfirmModel @Inject constructor( private fun updateConfirmNotifications() { modelScope.launch { + val feeCryptoCurrencyStatus = getCurrencyStatusForFeePayment() notificationsUpdateTrigger.triggerUpdate( data = NotificationData( destinationAddress = confirmData.enteredDestination.orEmpty(), @@ -512,9 +517,10 @@ internal class SendConfirmModel @Inject constructor( isIgnoreReduce = confirmData.isIgnoreReduce, fee = confirmData.fee, feeError = confirmData.feeError, - feeCryptoCurrencyStatus = getCurrencyStatusForFeePayment(), + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ), ) + val isHighNetworkFee = isHighNetworkFee(feeCryptoCurrencyStatus.currency) _uiState.update { state -> state.copy( confirmUM = SendConfirmationNotificationsTransformerV2( @@ -524,12 +530,19 @@ internal class SendConfirmModel @Inject constructor( cryptoCurrency = cryptoCurrencyStatus.currency, appCurrency = appCurrency, analyticsCategoryName = params.analyticsCategoryName, + isHighNetworkFee = isHighNetworkFee, ).transform(uiState.value.confirmUM), ) } } } + private suspend fun isHighNetworkFee(feeCurrency: CryptoCurrency): Boolean { + if (!sendFeatureToggles.isHighFeeWarningEnabled) return false + val feeAmount = confirmData.fee?.amount?.value ?: return false + return isHighNetworkFeeUseCase(feeCurrency, feeAmount) + } + @Suppress("LongMethod") private fun configConfirmNavigation() { combine( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt b/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt index f6e8b1f7e8..e240d132d3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt @@ -29,6 +29,7 @@ internal class SendConfirmationNotificationsTransformerV2( private val cryptoCurrency: CryptoCurrency, private val appCurrency: AppCurrency, private val analyticsCategoryName: String, + private val isHighNetworkFee: Boolean = false, ) : Transformer { override fun transform(prevState: ConfirmUM): ConfirmUM { val state = prevState as? ConfirmUM.Content ?: return prevState @@ -38,10 +39,17 @@ internal class SendConfirmationNotificationsTransformerV2( notifications = buildList { addTooHighNotification(feeSelectorUM) addTooLowNotification(feeSelectorUM) + addHighNetworkFeeNotification() }.toPersistentList(), ) } + private fun MutableList.addHighNetworkFeeNotification() { + if (isHighNetworkFee) { + add(NotificationUM.Warning.HighNetworkFee) + } + } + private fun MutableList.addTooLowNotification(feeSelectorUM: FeeSelectorUM.Content) { if (FeeCalculationUtils.checkIfCustomFeeTooLow(feeSelectorUM)) { add(NotificationUM.Warning.FeeTooLow) diff --git a/features/send/impl/src/test/java/com/tangem/features/send/send/SendModelTestBase.kt b/features/send/impl/src/test/java/com/tangem/features/send/send/SendModelTestBase.kt index db5b2e13d6..8658c5b023 100644 --- a/features/send/impl/src/test/java/com/tangem/features/send/send/SendModelTestBase.kt +++ b/features/send/impl/src/test/java/com/tangem/features/send/send/SendModelTestBase.kt @@ -23,6 +23,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase +import com.tangem.domain.quotes.IsHighNetworkFeeUseCase import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase @@ -30,6 +31,7 @@ import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.api.SendComponent +import com.tangem.features.send.api.SendFeatureToggles import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.api.entity.PredefinedValues import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener @@ -122,6 +124,8 @@ internal abstract class SendModelTestBase { protected val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase = mockk(relaxed = true) protected val currenciesRepository: CurrenciesRepository = mockk(relaxed = true) protected val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase = mockk(relaxed = true) + protected val isHighNetworkFeeUseCase: IsHighNetworkFeeUseCase = mockk(relaxed = true) + protected val sendFeatureToggles: SendFeatureToggles = mockk(relaxed = true) protected val sendAnalyticHelper: SendAnalyticHelper = mockk(relaxed = true) protected val sendBalanceUpdaterFactory: SendBalanceUpdater.Factory = mockk(relaxed = true) @@ -233,6 +237,8 @@ internal abstract class SendModelTestBase { manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase, currenciesRepository = currenciesRepository, createAndSendGaslessTransactionUseCase = createAndSendGaslessTransactionUseCase, + isHighNetworkFeeUseCase = isHighNetworkFeeUseCase, + sendFeatureToggles = sendFeatureToggles, sendBalanceUpdaterFactory = sendBalanceUpdaterFactory, ) } diff --git a/features/send/impl/src/test/java/com/tangem/features/send/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt b/features/send/impl/src/test/java/com/tangem/features/send/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt index 05cd13af6c..8c82cb540d 100644 --- a/features/send/impl/src/test/java/com/tangem/features/send/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt +++ b/features/send/impl/src/test/java/com/tangem/features/send/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt @@ -76,6 +76,7 @@ class SendConfirmationNotificationsTransformerV2Test { cryptoCurrency = cryptoCurrency, appCurrency = appCurrency, analyticsCategoryName = analyticsCategoryName, + isHighNetworkFee = false, ) val initialState: ConfirmUM = ConfirmUM.Empty @@ -98,6 +99,7 @@ class SendConfirmationNotificationsTransformerV2Test { cryptoCurrency = cryptoCurrency, appCurrency = appCurrency, analyticsCategoryName = analyticsCategoryName, + isHighNetworkFee = false, ) val initialState = createTestConfirmUM() @@ -120,6 +122,7 @@ class SendConfirmationNotificationsTransformerV2Test { cryptoCurrency = cryptoCurrency, appCurrency = appCurrency, analyticsCategoryName = analyticsCategoryName, + isHighNetworkFee = false, ) val initialState = createTestConfirmUM() @@ -145,6 +148,7 @@ class SendConfirmationNotificationsTransformerV2Test { cryptoCurrency = cryptoCurrency, appCurrency = appCurrency, analyticsCategoryName = analyticsCategoryName, + isHighNetworkFee = false, ) val initialState = createTestConfirmUM() @@ -158,6 +162,31 @@ class SendConfirmationNotificationsTransformerV2Test { assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java) } + @Test + fun `GIVEN high network fee WHEN transform THEN returns state with high network fee notification`() = runTest { + // GIVEN + val feeSelectorUM = createNormalFeeSelectorUM() + val amountUM = createTestAmountUM() + val transformer = SendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + isHighNetworkFee = true, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).containsExactly(NotificationUM.Warning.HighNetworkFee) + } + @Test fun `GIVEN fee too low WHEN transform THEN returns state with too low notification`() = runTest { // GIVEN @@ -170,6 +199,7 @@ class SendConfirmationNotificationsTransformerV2Test { cryptoCurrency = cryptoCurrency, appCurrency = appCurrency, analyticsCategoryName = analyticsCategoryName, + isHighNetworkFee = false, ) val initialState = createTestConfirmUM() @@ -196,6 +226,7 @@ class SendConfirmationNotificationsTransformerV2Test { cryptoCurrency = cryptoCurrency, appCurrency = appCurrency, analyticsCategoryName = analyticsCategoryName, + isHighNetworkFee = false, ) val initialState = createTestConfirmUM() diff --git a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt index b0fb0a7b2c..7c26a78310 100644 --- a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt +++ b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt @@ -2,4 +2,5 @@ package com.tangem.features.swap.v2.api interface SwapFeatureToggles { val isSwapProviderFilterEnabled: Boolean + val isHighFeeWarningEnabled: Boolean } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt index cf73e0fed6..4104bbaab3 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt @@ -10,4 +10,7 @@ internal class DefaultSwapFeatureToggles @Inject constructor( ) : SwapFeatureToggles { override val isSwapProviderFilterEnabled: Boolean = featureToggles.isFeatureEnabled(FeatureToggles.AND_15009_SWAP_PROVIDER_FILTER_ENABLED) + + override val isHighFeeWarningEnabled: Boolean = + featureToggles.isFeatureEnabled(FeatureToggles.TWI_1367_HIGH_FEE_WARNING_ENABLED) } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 7ef6c2aa13..9c63c30efb 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -27,6 +27,7 @@ import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.isHotWallet +import com.tangem.domain.quotes.IsHighNetworkFeeUseCase import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection @@ -47,6 +48,7 @@ import com.tangem.features.send.api.subcomponents.destination.entity.Destination import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateListener import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateTrigger +import com.tangem.features.swap.v2.api.SwapFeatureToggles import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.SwapAmountReduceTrigger @@ -89,6 +91,8 @@ internal class SendWithSwapConfirmModel @Inject constructor( private val estimateFeeForTokenUseCase: EstimateFeeForTokenUseCase, private val estimateFeeForGaslessTxUseCase: EstimateFeeForGaslessTxUseCase, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, + private val isHighNetworkFeeUseCase: IsHighNetworkFeeUseCase, + private val swapFeatureToggles: SwapFeatureToggles, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val sendNotificationsUpdateTrigger: SendNotificationsUpdateTrigger, private val swapNotificationsUpdateTrigger: SwapNotificationsUpdateTrigger, @@ -467,12 +471,19 @@ internal class SendWithSwapConfirmModel @Inject constructor( feeValue = confirmData.fee?.amount?.value, ), ) + val isHighNetworkFee = isHighNetworkFee(feeCryptoCurrencyStatus.currency) uiState.transformerUpdate( - SendWithSwapConfirmationNotificationsTransformer(), + SendWithSwapConfirmationNotificationsTransformer(isHighNetworkFee = isHighNetworkFee), ) } } + private suspend fun isHighNetworkFee(feeCurrency: CryptoCurrency): Boolean { + if (!swapFeatureToggles.isHighFeeWarningEnabled) return false + val feeAmount = confirmData.fee?.amount?.value ?: return false + return isHighNetworkFeeUseCase(feeCurrency, feeAmount) + } + private fun subscribeOnNotificationUpdates() { combine( flow = sendNotificationsUpdateListener.hasErrorFlow, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt index c62ecc15db..5f7e8286c0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt @@ -22,7 +22,9 @@ import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toPersistentList -internal class SendWithSwapConfirmationNotificationsTransformer : Transformer { +internal class SendWithSwapConfirmationNotificationsTransformer( + private val isHighNetworkFee: Boolean, +) : Transformer { override fun transform(prevState: SendWithSwapUM): SendWithSwapUM { val confirmUM = prevState.confirmUM as? ConfirmUM.Content ?: return prevState val feeSelectorUM = prevState.feeSelectorUM as? FeeSelectorUM.Content ?: return prevState @@ -34,11 +36,18 @@ internal class SendWithSwapConfirmationNotificationsTransformer : Transformer.addHighNetworkFeeNotification() { + if (isHighNetworkFee) { + add(NotificationUM.Warning.HighNetworkFee) + } + } + private fun MutableList.addTooLowNotification(feeSelectorUM: FeeSelectorUM.Content) { if (checkIfCustomFeeTooLow(feeSelectorUM = feeSelectorUM)) { add(NotificationUM.Warning.FeeTooLow) diff --git a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt index 91abcfd896..033fbb8f15 100644 --- a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt +++ b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt @@ -10,4 +10,5 @@ interface SwapFeatureToggles { val isSwapPredefinedButtonsEnabled: Boolean val isExpressShareButtonEnabled: Boolean val isSwapBestDexRateEnabled: Boolean + val isHighFeeWarningEnabled: Boolean } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt index 1babbc9152..41d5567f61 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt @@ -52,4 +52,9 @@ internal class DefaultSwapFeatureToggles @Inject constructor( get() = featureTogglesManager.isFeatureEnabled( toggle = FeatureToggles.AND_15715_SWAP_BEST_DEX_RATE_ENABLED, ) && isSwapIntegratedApproveEnabled + + override val isHighFeeWarningEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled( + toggle = FeatureToggles.TWI_1367_HIGH_FEE_WARNING_ENABLED, + ) } \ 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 d46ad20a15..bd36406530 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 @@ -65,6 +65,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.pay.WithdrawalResult import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase +import com.tangem.domain.quotes.IsHighNetworkFeeUseCase import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions @@ -173,6 +174,7 @@ internal class SwapModel @Inject constructor( private val getSwapUiModeUseCase: GetSwapUiModeUseCase, private val setSwapUiModeUseCase: SetSwapUiModeUseCase, private val calculateAmountUseCase: CalculateAmountUseCase, + private val isHighNetworkFeeUseCase: IsHighNetworkFeeUseCase, ) : Model() { private val params = paramsContainer.require() @@ -1103,7 +1105,7 @@ internal class SwapModel @Inject constructor( ) } - private fun setupLoadedState( + private suspend fun setupLoadedState( provider: SwapProvider, state: SwapState, fromSwapCurrencyStatus: SwapCurrencyStatus, @@ -1127,7 +1129,7 @@ internal class SwapModel @Inject constructor( } } - private fun setupQuotesLoadedUiState(provider: SwapProvider, state: SwapState.QuotesLoadedState) { + private suspend fun setupQuotesLoadedUiState(provider: SwapProvider, state: SwapState.QuotesLoadedState) { val loadedStates = dataState.getLastLoadedSuccessStates() val additionalBadge = SwapProviderResolver.resolveBadge( provider = provider, @@ -1136,17 +1138,26 @@ internal class SwapModel @Inject constructor( state = state, isSwapBestDexRateEnabled = swapFeatureToggles.isSwapBestDexRateEnabled, ) + val swapFee = getSelectedSwapFee() uiState = stateBuilder.createQuotesLoadedState( uiStateHolder = uiState, quoteModel = state, feeCryptoCurrencyStatus = dataState.feePaidCryptoCurrency, swapProvider = provider, additionalBadge = additionalBadge, - swapFee = getSelectedSwapFee(), + swapFee = swapFee, feeError = feeSelectorRepository.state.value as? FeeSelectorUM.Error, + isHighNetworkFee = isHighNetworkFee(swapFee), ) } + private suspend fun isHighNetworkFee(swapFee: SwapFee?): Boolean { + if (!swapFeatureToggles.isHighFeeWarningEnabled) return false + swapFee ?: return false + val feeAmount = swapFee.fee.amount.value ?: return false + return isHighNetworkFeeUseCase(swapFee.selectedFeeToken.currency, feeAmount) + } + private fun sendAnalyticsForNotifications( provider: SwapProvider, fromToken: CryptoCurrencyStatus, @@ -2077,12 +2088,14 @@ internal class SwapModel @Inject constructor( } analyticsEventHandler.send(SwapEvents.ProviderChosen(provider)) uiState = stateBuilder.dismissBottomSheet(uiState) - setupLoadedState( - provider = provider, - state = swapState, - fromSwapCurrencyStatus = fromSwapCurrencyStatus, - toSwapCurrencyStatus = toSwapCurrencyStatus, - ) + modelScope.launch { + setupLoadedState( + provider = provider, + state = swapState, + fromSwapCurrencyStatus = fromSwapCurrencyStatus, + toSwapCurrencyStatus = toSwapCurrencyStatus, + ) + } } }, onProviderFilterSelect = { filterType -> @@ -2823,12 +2836,14 @@ internal class SwapModel @Inject constructor( } }, ) - setupLoadedState( - provider = provider, - state = swapState, - fromSwapCurrencyStatus = fromSwapCurrencyStatus, - toSwapCurrencyStatus = toSwapCurrencyStatus, - ) + modelScope.launch { + setupLoadedState( + provider = provider, + state = swapState, + fromSwapCurrencyStatus = fromSwapCurrencyStatus, + toSwapCurrencyStatus = toSwapCurrencyStatus, + ) + } } else { TangemLogger.e("loadFee: ${feeError.error}, isHidden = true") refreshTransferUIStateIfNeeded() diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 1d2ca68bb9..7f9c144c0f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -124,6 +124,7 @@ internal class SwapNotificationsFactory( swapFee: SwapFee?, feeError: GetFeeError?, appRouter: AppRouter, + isHighNetworkFee: Boolean = false, ): ImmutableList { val warnings = buildList { maybeAddFeeErrorNotification(feeCryptoCurrencyStatus, quoteModel, feeError) @@ -135,10 +136,17 @@ internal class SwapNotificationsFactory( maybeAddUnableCoverFeeWarning(quoteModel, feeCryptoCurrencyStatus, appRouter) maybeAddTransactionInProgressWarning(quoteModel) maybeAddPriceImpactNotification(quoteModel.priceImpact) + maybeAddHighNetworkFeeWarning(isHighNetworkFee) } return warnings.toPersistentList() } + private fun MutableList.maybeAddHighNetworkFeeWarning(isHighNetworkFee: Boolean) { + if (isHighNetworkFee) { + add(NotificationUM.Warning.HighNetworkFee) + } + } + private fun MutableList.maybeAddRentExemptionError(quoteModel: SwapState.QuotesLoadedState) { quoteModel.currencyCheck?.rentWarning?.let { add(NotificationUM.Solana.RentInfo(it)) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 1875936424..fc355d82e7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -577,6 +577,7 @@ internal class StateBuilder( additionalBadge: ProviderState.AdditionalBadge, swapFee: SwapFee?, feeError: FeeSelectorUM.Error?, + isHighNetworkFee: Boolean, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -590,6 +591,7 @@ internal class StateBuilder( swapFee = swapFee, feeError = feeError?.error, appRouter = appRouter, + isHighNetworkFee = isHighNetworkFee, ) val fromAccountTitleUM = when { diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapButtonTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapButtonTest.kt index e608eb2f17..86ac7453d5 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapButtonTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapButtonTest.kt @@ -109,6 +109,7 @@ internal class StateBuilderSwapButtonTest { additionalBadge = ProviderState.AdditionalBadge.Empty, swapFee = null, feeError = null, + isHighNetworkFee = false, ) assertThat(result.swapButton.isEnabled).isTrue() @@ -134,6 +135,7 @@ internal class StateBuilderSwapButtonTest { additionalBadge = ProviderState.AdditionalBadge.Empty, swapFee = null, feeError = null, + isHighNetworkFee = false, ) assertThat(result.swapButton.isEnabled).isFalse() @@ -163,6 +165,7 @@ internal class StateBuilderSwapButtonTest { additionalBadge = ProviderState.AdditionalBadge.Empty, swapFee = null, feeError = null, + isHighNetworkFee = false, ) assertThat(result.swapButton.isEnabled).isFalse() @@ -187,6 +190,7 @@ internal class StateBuilderSwapButtonTest { additionalBadge = ProviderState.AdditionalBadge.Empty, swapFee = buildSwapFee(), feeError = null, + isHighNetworkFee = false, ) assertThat(result.swapButton.isEnabled).isTrue() @@ -215,6 +219,7 @@ internal class StateBuilderSwapButtonTest { additionalBadge = ProviderState.AdditionalBadge.Empty, swapFee = buildSwapFee(), feeError = null, + isHighNetworkFee = false, ) assertThat(result.swapButton.isEnabled).isFalse() diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelTestBase.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelTestBase.kt index 6a80346c00..23510180fa 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelTestBase.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelTestBase.kt @@ -26,6 +26,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase +import com.tangem.domain.quotes.IsHighNetworkFeeUseCase import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.stories.ShouldShowStoriesUseCase @@ -103,6 +104,7 @@ internal abstract class SwapModelTestBase { protected val getSwapUiModeUseCase: GetSwapUiModeUseCase = mockk(relaxed = true) protected val setSwapUiModeUseCase: SetSwapUiModeUseCase = mockk(relaxed = true) protected val calculateAmountUseCase: CalculateAmountUseCase = mockk(relaxed = true) + protected val isHighNetworkFeeUseCase: IsHighNetworkFeeUseCase = mockk(relaxed = true) protected val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase = mockk(relaxed = true) protected val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase = mockk(relaxed = true) @@ -175,6 +177,7 @@ internal abstract class SwapModelTestBase { getSwapUiModeUseCase = getSwapUiModeUseCase, setSwapUiModeUseCase = setSwapUiModeUseCase, calculateAmountUseCase = calculateAmountUseCase, + isHighNetworkFeeUseCase = isHighNetworkFeeUseCase, isWalletBackupProblematicUseCase = isWalletBackupProblematicUseCase, sendBackupProblemEmailUseCase = sendBackupProblemEmailUseCase, )