Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-16 20:41:42 +04:00
parent 50332186fa
commit 5f6767abaf
58 changed files with 85 additions and 717 deletions

View file

@ -198,20 +198,9 @@ abstract class BaseTestCase : TestCase(
// Version-gated toggles released in versions <= 6.0 — forced on so tests run against the actual // Version-gated toggles released in versions <= 6.0 — forced on so tests run against the actual
// build even when the app version resolves to 1.0.0-SNAPSHOT on CI (then 1.0.0 < x.xx would // build even when the app version resolves to 1.0.0-SNAPSHOT on CI (then 1.0.0 < x.xx would
// disable them). On the releases/6.0 branch every toggle with version <= 6.0 ships enabled. // disable them). On the releases/6.0 branch every toggle with version <= 6.0 ships enabled.
// 5.40
"TWI_1377_MANAGE_FUNDS" to true,
// 6.0 // 6.0
"APP_REDESIGN_ENABLED" to true, "APP_REDESIGN_ENABLED" to true,
"TWI_1326_YIELD_MODE_SWAP_ENABLED" to true, "TWI_1326_YIELD_MODE_SWAP_ENABLED" to true,
"AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED" to true,
"AND_15120_SWAP_INTEGRATED_APPROVE" to true,
"AND_15596_ONBOARDING_PUSH_NOTIFICATION_DOUBLE_ASK_AB_ENABLED" to true,
"AND_15258_QUICK_TOP_UP_ENABLED" to true,
"AND_15368_VISA_PAY_REDESIGN" to true,
"AND_15364_VISA_PAY_CARD_CLOSE" to true,
"AND_15489_EXPRESS_SHARE_BUTTON_ENABLED" to true,
"AND_15235_VISA_MULTIPLE_CARDS" to true,
"AND_15715_SWAP_BEST_DEX_RATE_ENABLED" to true,
// 6.1 // 6.1
"TWI_1638_VA_MVP0_ENABLED" to true, "TWI_1638_VA_MVP0_ENABLED" to true,
) )

View file

@ -39,26 +39,10 @@
"name": "ADDRESS_SYNC_ENABLED", "name": "ADDRESS_SYNC_ENABLED",
"version": "undefined" "version": "undefined"
}, },
{
"name": "AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED",
"version": "6.0"
},
{
"name": "AND_15120_SWAP_INTEGRATED_APPROVE",
"version": "6.0"
},
{ {
"name": "TWI_1403_PUSH_NOTIFICATION_SETTINGS_ENABLED", "name": "TWI_1403_PUSH_NOTIFICATION_SETTINGS_ENABLED",
"version": "undefined" "version": "undefined"
}, },
{
"name": "AND_15596_ONBOARDING_PUSH_NOTIFICATION_DOUBLE_ASK_AB_ENABLED",
"version": "6.0"
},
{
"name": "TWI_1377_MANAGE_FUNDS",
"version": "6.0"
},
{ {
"name": "AND_15438_BACKEND_AUTHENTICATION_ENABLED", "name": "AND_15438_BACKEND_AUTHENTICATION_ENABLED",
"version": "undefined" "version": "undefined"
@ -67,18 +51,6 @@
"name": "AND_15482_SURVEYSPARROW_ENABLED", "name": "AND_15482_SURVEYSPARROW_ENABLED",
"version": "undefined" "version": "undefined"
}, },
{
"name": "AND_15258_QUICK_TOP_UP_ENABLED",
"version": "6.0"
},
{
"name": "AND_15368_VISA_PAY_REDESIGN",
"version": "6.0"
},
{
"name": "AND_15364_VISA_PAY_CARD_CLOSE",
"version": "6.0"
},
{ {
"name": "AND_15741_VISA_PAY_REMOVE_ACCOUNT", "name": "AND_15741_VISA_PAY_REMOVE_ACCOUNT",
"version": "undefined" "version": "undefined"
@ -95,18 +67,6 @@
"name": "AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED", "name": "AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED",
"version": "undefined" "version": "undefined"
}, },
{
"name": "AND_15489_EXPRESS_SHARE_BUTTON_ENABLED",
"version": "6.0"
},
{
"name": "AND_15235_VISA_MULTIPLE_CARDS",
"version": "6.0"
},
{
"name": "AND_15715_SWAP_BEST_DEX_RATE_ENABLED",
"version": "6.0"
},
{ {
"name": "AND_15767_NEW_TX_HISTORY_ENABLED", "name": "AND_15767_NEW_TX_HISTORY_ENABLED",
"version": "undefined" "version": "undefined"
@ -115,10 +75,6 @@
"name": "AND_14829_WARNINGS_REFACTORING_ENABLED", "name": "AND_14829_WARNINGS_REFACTORING_ENABLED",
"version": "undefined" "version": "undefined"
}, },
{
"name": "AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED",
"version": "6.0"
},
{ {
"name": "TWI_1522_MARKETING_BANNERS_ENABLED", "name": "TWI_1522_MARKETING_BANNERS_ENABLED",
"version": "undefined" "version": "undefined"

View file

@ -14,12 +14,6 @@ internal class DefaultStakingFeatureToggles(
return featureTogglesManager.isFeatureEnabled(toggle) return featureTogglesManager.isFeatureEnabled(toggle)
} }
override fun isSolanaUnstakeValidationEnabled(): Boolean {
return featureTogglesManager.isFeatureEnabled(
FeatureToggles.AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED,
)
}
override fun isRegionUnavailableHandlingEnabled(): Boolean { override fun isRegionUnavailableHandlingEnabled(): Boolean {
return featureTogglesManager.isFeatureEnabled( return featureTogglesManager.isFeatureEnabled(
FeatureToggles.AND_15231_STAKING_REGION_UNAVAILABLE_ENABLED, FeatureToggles.AND_15231_STAKING_REGION_UNAVAILABLE_ENABLED,

View file

@ -48,32 +48,6 @@ internal class DefaultStakingFeatureTogglesTest {
verify(exactly = 0) { featureTogglesManager.isFeatureEnabled(any()) } verify(exactly = 0) { featureTogglesManager.isFeatureEnabled(any()) }
} }
@Test
fun `isSolanaUnstakeValidationEnabled returns true when toggle enabled`() {
every {
featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED)
} returns true
assertThat(toggles.isSolanaUnstakeValidationEnabled()).isTrue()
verify(exactly = 1) {
featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED)
}
}
@Test
fun `isSolanaUnstakeValidationEnabled returns false when toggle disabled`() {
every {
featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED)
} returns false
assertThat(toggles.isSolanaUnstakeValidationEnabled()).isFalse()
verify(exactly = 1) {
featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED)
}
}
@Test @Test
fun `GIVEN toggle enabled WHEN isRegionUnavailableHandlingEnabled THEN returns true`() { fun `GIVEN toggle enabled WHEN isRegionUnavailableHandlingEnabled THEN returns true`() {
// Arrange // Arrange

View file

@ -6,7 +6,5 @@ interface StakingFeatureToggles {
fun isIntegrationEnabled(integrationId: StakingIntegrationID): Boolean fun isIntegrationEnabled(integrationId: StakingIntegrationID): Boolean
fun isSolanaUnstakeValidationEnabled(): Boolean
fun isRegionUnavailableHandlingEnabled(): Boolean fun isRegionUnavailableHandlingEnabled(): Boolean
} }

View file

@ -1,7 +0,0 @@
package com.tangem.features.pushnotifications
interface PushNotificationsFeatureToggles {
/** Kill switch for the onboarding "Double Ask" A/B experiment (`twi_1403_onboarding_push_notification_double_ask`). */
val isOnboardingPushDoubleAskAbEnabled: Boolean
}

View file

@ -1,16 +0,0 @@
package com.tangem.features.pushnotifications.impl
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.pushnotifications.PushNotificationsFeatureToggles
import javax.inject.Inject
internal class DefaultPushNotificationsFeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) : PushNotificationsFeatureToggles {
override val isOnboardingPushDoubleAskAbEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
FeatureToggles.AND_15596_ONBOARDING_PUSH_NOTIFICATION_DOUBLE_ASK_AB_ENABLED,
)
}

View file

@ -1,12 +1,10 @@
package com.tangem.features.pushnotifications.impl.di package com.tangem.features.pushnotifications.impl.di
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.features.pushnotifications.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.api.PushNotificationsComponent import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.impl.DefaultPushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.impl.DefaultPushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.impl.DefaultPushNotificationsComponent import com.tangem.features.pushnotifications.impl.DefaultPushNotificationsComponent
import com.tangem.features.pushnotifications.impl.DefaultPushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.impl.model.PushNotificationsModel import com.tangem.features.pushnotifications.impl.model.PushNotificationsModel
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
@ -14,7 +12,6 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@ -32,8 +29,4 @@ internal interface PushNotificationsModule {
@IntoMap @IntoMap
@ClassKey(PushNotificationsModel::class) @ClassKey(PushNotificationsModel::class)
fun bindModel(model: PushNotificationsModel): Model fun bindModel(model: PushNotificationsModel): Model
@Binds
@Singleton
fun bindFeatureToggles(impl: DefaultPushNotificationsFeatureToggles): PushNotificationsFeatureToggles
} }

View file

@ -1,18 +1,13 @@
package com.tangem.features.pushnotifications.impl.domain package com.tangem.features.pushnotifications.impl.domain
import com.tangem.core.abtests.manager.ABTestsManager import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.features.pushnotifications.PushNotificationsFeatureToggles
import javax.inject.Inject import javax.inject.Inject
class GetPushNotificationsDoubleAskVariantUseCase @Inject constructor( class GetPushNotificationsDoubleAskVariantUseCase @Inject constructor(
private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles,
private val abTestsManager: ABTestsManager, private val abTestsManager: ABTestsManager,
) { ) {
suspend operator fun invoke(): DoubleAskVariant { suspend operator fun invoke(): DoubleAskVariant {
if (!pushNotificationsFeatureToggles.isOnboardingPushDoubleAskAbEnabled) {
return DoubleAskVariant.Off
}
val variant = abTestsManager.getValue(AMPLITUDE_ID, DoubleAskVariant.Off.key) val variant = abTestsManager.getValue(AMPLITUDE_ID, DoubleAskVariant.Off.key)
return DoubleAskVariant.fromKey(variant) return DoubleAskVariant.fromKey(variant)
} }

View file

@ -2,37 +2,22 @@ package com.tangem.features.pushnotifications.impl.domain
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import com.tangem.core.abtests.manager.ABTestsManager import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.features.pushnotifications.PushNotificationsFeatureToggles
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
internal class GetPushNotificationsDoubleAskVariantUseCaseTest { internal class GetPushNotificationsDoubleAskVariantUseCaseTest {
private val featureToggles: PushNotificationsFeatureToggles = mockk()
private val abTestsManager: ABTestsManager = mockk() private val abTestsManager: ABTestsManager = mockk()
private val useCase = GetPushNotificationsDoubleAskVariantUseCase( private val useCase = GetPushNotificationsDoubleAskVariantUseCase(
pushNotificationsFeatureToggles = featureToggles,
abTestsManager = abTestsManager, abTestsManager = abTestsManager,
) )
@Test @Test
fun `GIVEN toggle disabled WHEN invoke THEN returns Off and AB not queried`() = runTest { fun `GIVEN AB returns treatment WHEN invoke THEN returns On`() = runTest {
every { featureToggles.isOnboardingPushDoubleAskAbEnabled } returns false
val result = useCase()
assertThat(result).isEqualTo(DoubleAskVariant.Off)
coVerify(exactly = 0) { abTestsManager.getValue(any(), any()) }
}
@Test
fun `GIVEN toggle enabled AND AB returns treatment WHEN invoke THEN returns On`() = runTest {
every { featureToggles.isOnboardingPushDoubleAskAbEnabled } returns true
coEvery { abTestsManager.getValue(KEY, "control") } returns "treatment" coEvery { abTestsManager.getValue(KEY, "control") } returns "treatment"
val result = useCase() val result = useCase()
@ -42,16 +27,14 @@ internal class GetPushNotificationsDoubleAskVariantUseCaseTest {
} }
@Test @Test
fun `GIVEN toggle enabled AND AB returns control WHEN invoke THEN returns Off`() = runTest { fun `GIVEN AB returns control WHEN invoke THEN returns Off`() = runTest {
every { featureToggles.isOnboardingPushDoubleAskAbEnabled } returns true
coEvery { abTestsManager.getValue(KEY, "control") } returns "control" coEvery { abTestsManager.getValue(KEY, "control") } returns "control"
assertThat(useCase()).isEqualTo(DoubleAskVariant.Off) assertThat(useCase()).isEqualTo(DoubleAskVariant.Off)
} }
@Test @Test
fun `GIVEN toggle enabled AND AB returns unknown WHEN invoke THEN returns Off`() = runTest { fun `GIVEN AB returns unknown WHEN invoke THEN returns Off`() = runTest {
every { featureToggles.isOnboardingPushDoubleAskAbEnabled } returns true
coEvery { abTestsManager.getValue(KEY, "control") } returns "unexpected_value" coEvery { abTestsManager.getValue(KEY, "control") } returns "unexpected_value"
assertThat(useCase()).isEqualTo(DoubleAskVariant.Off) assertThat(useCase()).isEqualTo(DoubleAskVariant.Off)

View file

@ -60,7 +60,6 @@ import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.P2PEthPoolRepository import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.tokens.* import com.tangem.domain.tokens.*
import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase
@ -156,7 +155,6 @@ internal class StakingModel @Inject constructor(
private val coroutineScope: AppCoroutineScope, private val coroutineScope: AppCoroutineScope,
private val innerRouter: InnerStakingRouter, private val innerRouter: InnerStakingRouter,
private val messageSender: UiMessageSender, private val messageSender: UiMessageSender,
private val stakingFeatureToggles: StakingFeatureToggles,
private val appRouter: AppRouter, private val appRouter: AppRouter,
) : Model(), StakingClickIntents { ) : Model(), StakingClickIntents {
@ -399,8 +397,6 @@ internal class StakingModel @Inject constructor(
minimumTransactionAmount = minimumTransactionAmount, minimumTransactionAmount = minimumTransactionAmount,
actionType = uiState.value.actionType, actionType = uiState.value.actionType,
integration = integration, integration = integration,
isSolanaUnstakeValidationEnabled = stakingFeatureToggles
.isSolanaUnstakeValidationEnabled(),
) )
addAll( addAll(
@ -663,7 +659,6 @@ internal class StakingModel @Inject constructor(
minimumTransactionAmount = minimumTransactionAmount, minimumTransactionAmount = minimumTransactionAmount,
value = value, value = value,
integration = integration, integration = integration,
isSolanaUnstakeValidationEnabled = stakingFeatureToggles.isSolanaUnstakeValidationEnabled(),
), ),
) )
checkSumLimitExceeded() checkSumLimitExceeded()
@ -705,7 +700,6 @@ internal class StakingModel @Inject constructor(
minimumTransactionAmount = minimumTransactionAmount, minimumTransactionAmount = minimumTransactionAmount,
actionType = uiState.value.actionType, actionType = uiState.value.actionType,
integration = integration, integration = integration,
isSolanaUnstakeValidationEnabled = stakingFeatureToggles.isSolanaUnstakeValidationEnabled(),
), ),
) )
checkSumLimitExceeded() checkSumLimitExceeded()
@ -1388,7 +1382,6 @@ internal class StakingModel @Inject constructor(
value = amountValue, value = amountValue,
minimumTransactionAmount = minimumTransactionAmount, minimumTransactionAmount = minimumTransactionAmount,
integration = integration, integration = integration,
isSolanaUnstakeValidationEnabled = stakingFeatureToggles.isSolanaUnstakeValidationEnabled(),
), ),
) )
} }

View file

@ -14,7 +14,6 @@ internal class AmountChangeStateTransformer(
private val minimumTransactionAmount: EnterAmountBoundary?, private val minimumTransactionAmount: EnterAmountBoundary?,
private val value: String, private val value: String,
private val integration: StakingIntegration, private val integration: StakingIntegration,
private val isSolanaUnstakeValidationEnabled: Boolean,
) : Transformer<StakingUiState> { ) : Transformer<StakingUiState> {
private val maxEnterAmountConverter = MaxEnterAmountConverter() private val maxEnterAmountConverter = MaxEnterAmountConverter()
@ -44,7 +43,6 @@ internal class AmountChangeStateTransformer(
maxAmount = maxEnterAmount, maxAmount = maxEnterAmount,
integration = integration, integration = integration,
actionType = prevState.actionType, actionType = prevState.actionType,
isSolanaUnstakeValidationEnabled = isSolanaUnstakeValidationEnabled,
).transform(updatedAmountState), ).transform(updatedAmountState),
) )
} }

View file

@ -14,7 +14,6 @@ internal class AmountMaxValueStateTransformer(
private val minimumTransactionAmount: EnterAmountBoundary?, private val minimumTransactionAmount: EnterAmountBoundary?,
private val actionType: StakingActionCommonType, private val actionType: StakingActionCommonType,
private val integration: StakingIntegration, private val integration: StakingIntegration,
private val isSolanaUnstakeValidationEnabled: Boolean,
) : Transformer<StakingUiState> { ) : Transformer<StakingUiState> {
private val maxEnterAmountConverter = MaxEnterAmountConverter() private val maxEnterAmountConverter = MaxEnterAmountConverter()
@ -41,7 +40,6 @@ internal class AmountMaxValueStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
integration = integration, integration = integration,
actionType = prevState.actionType, actionType = prevState.actionType,
isSolanaUnstakeValidationEnabled = isSolanaUnstakeValidationEnabled,
).transform(updatedAmountState), ).transform(updatedAmountState),
) )
} }

View file

@ -29,7 +29,6 @@ internal class AmountRequirementStateTransformer(
private val maxAmount: EnterAmountBoundary, private val maxAmount: EnterAmountBoundary,
private val integration: StakingIntegration, private val integration: StakingIntegration,
private val actionType: StakingActionCommonType, private val actionType: StakingActionCommonType,
private val isSolanaUnstakeValidationEnabled: Boolean = false,
) : Transformer<AmountState> { ) : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState { override fun transform(prevState: AmountState): AmountState {
return if (prevState is AmountState.Data) { return if (prevState is AmountState.Data) {
@ -105,9 +104,7 @@ internal class AmountRequirementStateTransformer(
) )
} }
is StakingActionCommonType.Exit -> { is StakingActionCommonType.Exit -> {
if (isSolanaUnstakeValidationEnabled && if (isSolana(cryptoCurrencyStatus.currency.network.rawId)) {
isSolana(cryptoCurrencyStatus.currency.network.rawId)
) {
getSolanaUnstakeError(amount = amountDecimal, staked = maxAmount.amount) getSolanaUnstakeError(amount = amountDecimal, staked = maxAmount.amount)
} else { } else {
integration.exitArgs?.amountRequirement?.getError( integration.exitArgs?.amountRequirement?.getError(

View file

@ -29,7 +29,6 @@ import com.tangem.domain.staking.*
import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.repositories.P2PEthPoolRepository import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.tokens.* import com.tangem.domain.tokens.*
import com.tangem.domain.transaction.usecase.* import com.tangem.domain.transaction.usecase.*
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
@ -114,9 +113,6 @@ internal abstract class StakingModelTestBase {
private val coroutineScope: AppCoroutineScope = mockk() private val coroutineScope: AppCoroutineScope = mockk()
protected val innerRouter: InnerStakingRouter = mockk() protected val innerRouter: InnerStakingRouter = mockk()
protected val messageSender: UiMessageSender = mockk() protected val messageSender: UiMessageSender = mockk()
protected val stakingFeatureToggles: StakingFeatureToggles = mockk {
every { isSolanaUnstakeValidationEnabled() } returns false
}
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
@ -208,7 +204,6 @@ internal abstract class StakingModelTestBase {
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
innerRouter = innerRouter, innerRouter = innerRouter,
messageSender = messageSender, messageSender = messageSender,
stakingFeatureToggles = stakingFeatureToggles,
appRouter = appRouter, appRouter = appRouter,
) )
} }

View file

@ -85,13 +85,11 @@ internal class AmountRequirementStateTransformerTest {
staked: BigDecimal, staked: BigDecimal,
exitMin: BigDecimal?, exitMin: BigDecimal?,
enterMin: BigDecimal? = null, enterMin: BigDecimal? = null,
enabled: Boolean = true,
) = AmountRequirementStateTransformer( ) = AmountRequirementStateTransformer(
cryptoCurrencyStatus = solanaCryptoStatus(), cryptoCurrencyStatus = solanaCryptoStatus(),
maxAmount = EnterAmountBoundary(amount = staked, fiatAmount = null, fiatRate = null), maxAmount = EnterAmountBoundary(amount = staked, fiatAmount = null, fiatRate = null),
integration = solanaExitIntegration(exitMin = exitMin, enterMin = enterMin), integration = solanaExitIntegration(exitMin = exitMin, enterMin = enterMin),
actionType = StakingActionCommonType.Exit(partiallyUnstakeDisabled = false), actionType = StakingActionCommonType.Exit(partiallyUnstakeDisabled = false),
isSolanaUnstakeValidationEnabled = enabled,
) )
@Test @Test
@ -285,26 +283,12 @@ internal class AmountRequirementStateTransformerTest {
} }
@Test @Test
fun `WHEN Solana validation disabled THEN partial unstake below minimum allowed`() { fun `WHEN currency is not Solana THEN Solana rule does not apply`() {
val transformer = solanaTransformer(
staked = BigDecimal("5"),
exitMin = BigDecimal("1"),
enabled = false,
)
val result = transformer.transform(amountState(BigDecimal("0.5"))) as AmountState.Data
assertThat(result.amountTextField.isError).isFalse()
}
@Test
fun `WHEN validation enabled but currency is not Solana THEN Solana rule does not apply`() {
val transformer = AmountRequirementStateTransformer( val transformer = AmountRequirementStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus, // relaxed mock: network.rawId is not "solana" cryptoCurrencyStatus = cryptoCurrencyStatus, // relaxed mock: network.rawId is not "solana"
maxAmount = EnterAmountBoundary(amount = BigDecimal("5"), fiatAmount = null, fiatRate = null), maxAmount = EnterAmountBoundary(amount = BigDecimal("5"), fiatAmount = null, fiatRate = null),
integration = exitIntegrationWith(minimum = null, maximum = null), integration = exitIntegrationWith(minimum = null, maximum = null),
actionType = StakingActionCommonType.Exit(partiallyUnstakeDisabled = false), actionType = StakingActionCommonType.Exit(partiallyUnstakeDisabled = false),
isSolanaUnstakeValidationEnabled = true,
) )
val result = transformer.transform(amountState(BigDecimal("0.5"))) as AmountState.Data val result = transformer.transform(amountState(BigDecimal("0.5"))) as AmountState.Data

View file

@ -2,10 +2,6 @@ package com.tangem.features.swap
interface SwapFeatureToggles { interface SwapFeatureToggles {
val isYieldSwapEnabled: Boolean val isYieldSwapEnabled: Boolean
val isSwapSwitchToTransferEnabled: Boolean
val isSwapIntegratedApproveEnabled: Boolean
val isExpressShareButtonEnabled: Boolean
val isSwapBestDexRateEnabled: Boolean
val isHighFeeWarningEnabled: Boolean val isHighFeeWarningEnabled: Boolean
val isTronDexSwapEnabled: Boolean val isTronDexSwapEnabled: Boolean
} }

View file

@ -157,9 +157,7 @@ interface SwapInteractor {
* `LIMITED`), loads its [com.tangem.blockchain.common.transaction.TransactionFee] and returns * `LIMITED`), loads its [com.tangem.blockchain.common.transaction.TransactionFee] and returns
* both as [IntegratedApprovalData]. * both as [IntegratedApprovalData].
* *
* Used by the integrated approve+swap flow when * Used by the integrated approve+swap flow when the quote requires an allowance bump.
* `SwapFeatureToggles.isSwapIntegratedApproveEnabled` is ON and the quote requires an
* allowance bump.
* *
* @param approvalAmount LIMITED-mode swap amount (the user-input amount). Used when * @param approvalAmount LIMITED-mode swap amount (the user-input amount). Used when
* [approveType] is `LIMITED`; ignored for `UNLIMITED`. * [approveType] is `LIMITED`; ignored for `UNLIMITED`.

View file

@ -404,8 +404,7 @@ internal class SwapInteractorImpl @Inject constructor(
) )
} }
val isBalanceWithoutFeeEnough = isBalanceEnough(fromSwapCurrencyStatus, amount, null) val isBalanceWithoutFeeEnough = isBalanceEnough(fromSwapCurrencyStatus, amount, null)
val isIntegratedApproveActive = swapFeatureToggles.isSwapIntegratedApproveEnabled && val isIntegratedApproveActive = !hasIntegratedApprovalFallenBack(fromSwapCurrencyStatus, spenderAddress)
!hasIntegratedApprovalFallenBack(fromSwapCurrencyStatus, spenderAddress)
val isAllowanceSatisfied = if (isIntegratedApproveActive) { val isAllowanceSatisfied = if (isIntegratedApproveActive) {
allowanceInfo !is AllowanceInfo.ResetNeeded allowanceInfo !is AllowanceInfo.ResetNeeded
@ -1973,7 +1972,6 @@ internal class SwapInteractorImpl @Inject constructor(
val isYieldSwap = fromSwapCurrencyStatus.isYieldSwapActive && val isYieldSwap = fromSwapCurrencyStatus.isYieldSwapActive &&
fromSwapCurrencyStatus.currency is CryptoCurrency.Token fromSwapCurrencyStatus.currency is CryptoCurrency.Token
val isIntegratedApprovalNeeded = !isYieldSwap && val isIntegratedApprovalNeeded = !isYieldSwap &&
swapFeatureToggles.isSwapIntegratedApproveEnabled &&
allowanceInfo is AllowanceInfo.NotEnough && allowanceInfo is AllowanceInfo.NotEnough &&
!hasIntegratedApprovalFallenBack(fromSwapCurrencyStatus, spenderAddress) !hasIntegratedApprovalFallenBack(fromSwapCurrencyStatus, spenderAddress)
swapState.copy( swapState.copy(
@ -2120,7 +2118,6 @@ internal class SwapInteractorImpl @Inject constructor(
).getOrNull() ?: return quotesLoadedState.copy(permissionState = PermissionDataState.Empty) ).getOrNull() ?: return quotesLoadedState.copy(permissionState = PermissionDataState.Empty)
val isIntegratedApprovalNeeded = !isYieldSwap && val isIntegratedApprovalNeeded = !isYieldSwap &&
swapFeatureToggles.isSwapIntegratedApproveEnabled &&
allowanceInfo is AllowanceInfo.NotEnough && allowanceInfo is AllowanceInfo.NotEnough &&
!hasIntegratedApprovalFallenBack(fromSwapCurrencyStatus, quoteModel.allowanceContract) !hasIntegratedApprovalFallenBack(fromSwapCurrencyStatus, quoteModel.allowanceContract)
return quotesLoadedState.copy( return quotesLoadedState.copy(

View file

@ -46,7 +46,6 @@ import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.ui.SwapState import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkFeeCoverage import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkFeeCoverage
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.utils.extensions.orZero import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import java.math.BigDecimal import java.math.BigDecimal
@ -54,7 +53,6 @@ import javax.inject.Inject
@Suppress("LongParameterList", "LargeClass") @Suppress("LongParameterList", "LargeClass")
class SwapTransferInteractorImpl @Inject constructor( class SwapTransferInteractorImpl @Inject constructor(
private val swapFeatureToggles: SwapFeatureToggles,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
@ -306,7 +304,6 @@ class SwapTransferInteractorImpl @Inject constructor(
fromSwapCurrency: CryptoCurrency?, fromSwapCurrency: CryptoCurrency?,
toSwapCurrency: CryptoCurrency?, toSwapCurrency: CryptoCurrency?,
): Boolean { ): Boolean {
if (swapFeatureToggles.isSwapSwitchToTransferEnabled.not()) return false
val isSameCurrency = when { val isSameCurrency = when {
fromSwapCurrency is CryptoCurrency.Coin && toSwapCurrency is CryptoCurrency.Coin -> { fromSwapCurrency is CryptoCurrency.Coin && toSwapCurrency is CryptoCurrency.Coin -> {
fromSwapCurrency.network.rawId == toSwapCurrency.network.rawId fromSwapCurrency.network.rawId == toSwapCurrency.network.rawId

View file

@ -1618,11 +1618,6 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase(
private val spender = "0xDexRouter" private val spender = "0xDexRouter"
private val tokenContract = "0xRegularToken" private val tokenContract = "0xRegularToken"
@BeforeEach
fun enableIntegrated() {
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
}
@Test @Test
fun `NotEnough allowance with integrated active proceeds to PermissionSettings`() = runTest { fun `NotEnough allowance with integrated active proceeds to PermissionSettings`() = runTest {
stubAllowanceForSpender( stubAllowanceForSpender(

View file

@ -169,7 +169,6 @@ internal class SwapInteractorImplLoadDexSwapDataNoFeeTest : SwapInteractorImplTe
@Test @Test
fun `GIVEN NotEnough allowance AND integrated active THEN permissionState is PermissionSettings`() = runTest { fun `GIVEN NotEnough allowance AND integrated active THEN permissionState is PermissionSettings`() = runTest {
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
stubAllowance(AllowanceInfo.NotEnough(allowance = BigDecimal.ZERO, requiredAmount = BigDecimal.ONE)) stubAllowance(AllowanceInfo.NotEnough(allowance = BigDecimal.ZERO, requiredAmount = BigDecimal.ONE))
val state = runFindBestQuoteForToken() val state = runFindBestQuoteForToken()
@ -181,7 +180,6 @@ internal class SwapInteractorImplLoadDexSwapDataNoFeeTest : SwapInteractorImplTe
@Test @Test
fun `GIVEN Enough allowance THEN permissionState is Empty`() = runTest { fun `GIVEN Enough allowance THEN permissionState is Empty`() = runTest {
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
stubAllowance(AllowanceInfo.Enough(allowance = BigDecimal("100"))) stubAllowance(AllowanceInfo.Enough(allowance = BigDecimal("100")))
val state = runFindBestQuoteForToken() val state = runFindBestQuoteForToken()
@ -194,7 +192,6 @@ internal class SwapInteractorImplLoadDexSwapDataNoFeeTest : SwapInteractorImplTe
runTest { runTest {
// [REDACTED_TASK_KEY] / iOS parity: yield swaps must never use the integrated approve+swap path. // [REDACTED_TASK_KEY] / iOS parity: yield swaps must never use the integrated approve+swap path.
// The yield-module proxy allowance is granted at enrollment, so no in-flow approval is shown. // The yield-module proxy allowance is granted at enrollment, so no in-flow approval is shown.
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
every { swapFeatureToggles.isYieldSwapEnabled } returns true every { swapFeatureToggles.isYieldSwapEnabled } returns true
coEvery { yieldModuleAddressProvider.getOrFetch(any(), any()) } returns YIELD_PROXY coEvery { yieldModuleAddressProvider.getOrFetch(any(), any()) } returns YIELD_PROXY
coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), any()) } returns true coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), any()) } returns true
@ -221,26 +218,6 @@ internal class SwapInteractorImplLoadDexSwapDataNoFeeTest : SwapInteractorImplTe
assertThat(state.permissionState).isEqualTo(PermissionDataState.Empty) assertThat(state.permissionState).isEqualTo(PermissionDataState.Empty)
} }
@Test
fun `GIVEN NotEnough allowance AND integrated toggle OFF THEN does not reach loadDexSwapDataNoFee`() = runTest {
// With the integrated toggle off, NotEnough is not allowance-satisfied (requires Enough),
// so manageDex does NOT enter loadDexSwapDataNoFee — getExchangeData is never called.
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns false
stubAllowance(AllowanceInfo.NotEnough(allowance = BigDecimal.ZERO, requiredAmount = BigDecimal.ONE))
runFindBestQuoteForTokenRaw()
coVerify(exactly = 0) {
repository.getExchangeData(
userWallet = any(), fromContractAddress = any(), fromNetwork = any(),
toContractAddress = any(), fromAddress = any(), toNetwork = any(),
fromAmount = any(), fromDecimals = any(), toDecimals = any(),
providerId = any(), rateType = any(), toAddress = any(),
expressOperationType = any(), refundAddress = any(),
)
}
}
// endregion // endregion
private fun stubAllowance(info: AllowanceInfo) { private fun stubAllowance(info: AllowanceInfo) {
@ -261,11 +238,6 @@ internal class SwapInteractorImplLoadDexSwapDataNoFeeTest : SwapInteractorImplTe
return result[dexProvider] as SwapState.QuotesLoadedState return result[dexProvider] as SwapState.QuotesLoadedState
} }
private suspend fun runFindBestQuoteForTokenRaw() {
val dexProvider = stubDexQuoteAndExchangeData()
invokeFindBestQuote(dexProvider)
}
private fun stubDexQuoteAndExchangeData(): com.tangem.feature.swap.domain.models.domain.SwapProvider { private fun stubDexQuoteAndExchangeData(): com.tangem.feature.swap.domain.models.domain.SwapProvider {
val dexProvider = buildSwapProvider(ExchangeProviderType.DEX) val dexProvider = buildSwapProvider(ExchangeProviderType.DEX)
val quoteModel = buildQuoteModel(allowanceContract = SPENDER) val quoteModel = buildQuoteModel(allowanceContract = SPENDER)

View file

@ -38,7 +38,6 @@ import com.tangem.feature.swap.domain.fee.TransactionFeeResult
import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.ui.SwapState import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
import com.tangem.features.swap.SwapFeatureToggles
import io.mockk.* import io.mockk.*
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
@ -53,7 +52,6 @@ import java.math.BigDecimal
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class SwapTransferInteractorImplTest { internal class SwapTransferInteractorImplTest {
private val swapFeatureToggles: SwapFeatureToggles = mockk()
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk() private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk()
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase = mockk() private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase = mockk()
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase = mockk() private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase = mockk()
@ -72,7 +70,6 @@ internal class SwapTransferInteractorImplTest {
private val validateTransactionUseCase: ValidateTransactionUseCase = mockk() private val validateTransactionUseCase: ValidateTransactionUseCase = mockk()
private val sut = SwapTransferInteractorImpl( private val sut = SwapTransferInteractorImpl(
swapFeatureToggles = swapFeatureToggles,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
getBalanceHidingSettingsUseCase = getBalanceHidingSettingsUseCase, getBalanceHidingSettingsUseCase = getBalanceHidingSettingsUseCase,
isAccountsModeEnabledUseCase = isAccountsModeEnabledUseCase, isAccountsModeEnabledUseCase = isAccountsModeEnabledUseCase,
@ -1314,23 +1311,8 @@ internal class SwapTransferInteractorImplTest {
// region shouldTransferInsteadOfSwap // region shouldTransferInsteadOfSwap
@Test
fun `GIVEN feature toggle disabled WHEN shouldTransferInsteadOfSwap THEN return false`() {
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns false
val result = sut.shouldTransferInsteadOfSwap(
fromSwapCurrency = buildCoin(networkRawId = ETHEREUM),
toSwapCurrency = buildCoin(networkRawId = ETHEREUM),
)
assertThat(result).isFalse()
verify { swapFeatureToggles.isSwapSwitchToTransferEnabled }
}
@Test @Test
fun `GIVEN both coins on the same network WHEN shouldTransferInsteadOfSwap THEN return true`() { fun `GIVEN both coins on the same network WHEN shouldTransferInsteadOfSwap THEN return true`() {
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
val result = sut.shouldTransferInsteadOfSwap( val result = sut.shouldTransferInsteadOfSwap(
fromSwapCurrency = buildCoin(networkRawId = ETHEREUM), fromSwapCurrency = buildCoin(networkRawId = ETHEREUM),
toSwapCurrency = buildCoin(networkRawId = ETHEREUM), toSwapCurrency = buildCoin(networkRawId = ETHEREUM),
@ -1341,8 +1323,6 @@ internal class SwapTransferInteractorImplTest {
@Test @Test
fun `GIVEN coins on different networks WHEN shouldTransferInsteadOfSwap THEN return false`() { fun `GIVEN coins on different networks WHEN shouldTransferInsteadOfSwap THEN return false`() {
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
val result = sut.shouldTransferInsteadOfSwap( val result = sut.shouldTransferInsteadOfSwap(
fromSwapCurrency = buildCoin(networkRawId = ETHEREUM), fromSwapCurrency = buildCoin(networkRawId = ETHEREUM),
toSwapCurrency = buildCoin(networkRawId = POLYGON), toSwapCurrency = buildCoin(networkRawId = POLYGON),
@ -1353,8 +1333,6 @@ internal class SwapTransferInteractorImplTest {
@Test @Test
fun `GIVEN tokens with same network and same contract WHEN shouldTransferInsteadOfSwap THEN return true`() { fun `GIVEN tokens with same network and same contract WHEN shouldTransferInsteadOfSwap THEN return true`() {
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
val result = sut.shouldTransferInsteadOfSwap( val result = sut.shouldTransferInsteadOfSwap(
fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT), fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
toSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT), toSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
@ -1365,8 +1343,6 @@ internal class SwapTransferInteractorImplTest {
@Test @Test
fun `GIVEN tokens with same network but different contract WHEN shouldTransferInsteadOfSwap THEN return false`() { fun `GIVEN tokens with same network but different contract WHEN shouldTransferInsteadOfSwap THEN return false`() {
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
val result = sut.shouldTransferInsteadOfSwap( val result = sut.shouldTransferInsteadOfSwap(
fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT), fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
toSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDC_CONTRACT), toSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDC_CONTRACT),
@ -1377,8 +1353,6 @@ internal class SwapTransferInteractorImplTest {
@Test @Test
fun `GIVEN tokens with same contract but different network WHEN shouldTransferInsteadOfSwap THEN return false`() { fun `GIVEN tokens with same contract but different network WHEN shouldTransferInsteadOfSwap THEN return false`() {
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
val result = sut.shouldTransferInsteadOfSwap( val result = sut.shouldTransferInsteadOfSwap(
fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT), fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
toSwapCurrency = buildToken(networkRawId = POLYGON, contractAddress = USDT_CONTRACT), toSwapCurrency = buildToken(networkRawId = POLYGON, contractAddress = USDT_CONTRACT),
@ -1389,8 +1363,6 @@ internal class SwapTransferInteractorImplTest {
@Test @Test
fun `GIVEN coin from and token to WHEN shouldTransferInsteadOfSwap THEN return false`() { fun `GIVEN coin from and token to WHEN shouldTransferInsteadOfSwap THEN return false`() {
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
val result = sut.shouldTransferInsteadOfSwap( val result = sut.shouldTransferInsteadOfSwap(
fromSwapCurrency = buildCoin(networkRawId = ETHEREUM), fromSwapCurrency = buildCoin(networkRawId = ETHEREUM),
toSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT), toSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
@ -1401,8 +1373,6 @@ internal class SwapTransferInteractorImplTest {
@Test @Test
fun `GIVEN token from and coin to WHEN shouldTransferInsteadOfSwap THEN return false`() { fun `GIVEN token from and coin to WHEN shouldTransferInsteadOfSwap THEN return false`() {
every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
val result = sut.shouldTransferInsteadOfSwap( val result = sut.shouldTransferInsteadOfSwap(
fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT), fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
toSwapCurrency = buildCoin(networkRawId = ETHEREUM), toSwapCurrency = buildCoin(networkRawId = ETHEREUM),

View file

@ -14,26 +14,6 @@ internal class DefaultSwapFeatureToggles @Inject constructor(
toggle = FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED, toggle = FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED,
) )
override val isSwapSwitchToTransferEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED,
)
override val isSwapIntegratedApproveEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_15120_SWAP_INTEGRATED_APPROVE,
)
override val isExpressShareButtonEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_15489_EXPRESS_SHARE_BUTTON_ENABLED,
)
override val isSwapBestDexRateEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_15715_SWAP_BEST_DEX_RATE_ENABLED,
) && isSwapIntegratedApproveEnabled
override val isHighFeeWarningEnabled: Boolean override val isHighFeeWarningEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled( get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.TWI_1367_HIGH_FEE_WARNING_ENABLED, toggle = FeatureToggles.TWI_1367_HIGH_FEE_WARNING_ENABLED,

View file

@ -28,18 +28,11 @@ internal object SwapProviderResolver {
/** /**
* Picks the best provider among [states]. * Picks the best provider among [states].
* *
* When [isSwapBestDexRateEnabled] is on and at least one DEX/DEX_BRIDGE provider is present, the * When at least one DEX/DEX_BRIDGE provider is present, the best-rated DEX provider wins; otherwise
* best-rated DEX provider wins; otherwise the overall best-rated provider is returned (the best * the overall best-rated provider is returned (the best CEX when no DEX is available). "Best rated" =
* CEX when no DEX is available). "Best rated" = lowest from/to fiat ratio (most output per unit * lowest from/to fiat ratio (most output per unit of input). Returns null when [states] is empty.
* of input). Returns null when [states] is empty.
*
* @param isSwapBestDexRateEnabled whether the Best DEX Rate feature toggle is on.
*/ */
fun findBest( fun findBest(states: Map<SwapProvider, SwapState.QuotesLoadedState>): SwapProvider? {
states: Map<SwapProvider, SwapState.QuotesLoadedState>,
isSwapBestDexRateEnabled: Boolean,
): SwapProvider? {
if (!isSwapBestDexRateEnabled) return findBestRated(states)
val dexStates = states.filterKeys { it.type.isDex() } val dexStates = states.filterKeys { it.type.isDex() }
return if (dexStates.isNotEmpty()) { return if (dexStates.isNotEmpty()) {
findBestRated(dexStates) findBestRated(dexStates)
@ -54,7 +47,7 @@ internal object SwapProviderResolver {
* Priority: FCA restriction permission required recommended best rate none. A best-rate * Priority: FCA restriction permission required recommended best rate none. A best-rate
* badge is shown only when more than one provider is considered, FCA restrictions are not applied, * badge is shown only when more than one provider is considered, FCA restrictions are not applied,
* and this row's quote carries no price-impact warning. Which best-rate badge it is depends on the * and this row's quote carries no price-impact warning. Which best-rate badge it is depends on the
* provider mix (only relevant when [isSwapBestDexRateEnabled] is on): * provider mix:
* - [AdditionalBadge.BestTrade] ("Best rate") always on the overall best-rated provider, * - [AdditionalBadge.BestTrade] ("Best rate") always on the overall best-rated provider,
* regardless of its type. * regardless of its type.
* - [AdditionalBadge.BestDexRate] ("Best DEX rate") only when both CEX and DEX providers are * - [AdditionalBadge.BestDexRate] ("Best DEX rate") only when both CEX and DEX providers are
@ -62,22 +55,17 @@ internal object SwapProviderResolver {
* shown on the best-rated DEX. When a DEX already is the overall best, or the set is CEX-only / * shown on the best-rated DEX. When a DEX already is the overall best, or the set is CEX-only /
* DEX-only, no separate "Best DEX rate" badge is shown. * DEX-only, no separate "Best DEX rate" badge is shown.
* *
* When [isSwapBestDexRateEnabled] is off, only the overall best provider gets [AdditionalBadge.BestTrade]
* (legacy behaviour) and [AdditionalBadge.BestDexRate] is never produced.
*
* @param states all loaded quotes used to find the best providers and to count considered providers. * @param states all loaded quotes used to find the best providers and to count considered providers.
* @param provider the provider this row represents. * @param provider the provider this row represents.
* @param needApplyFCARestrictions whether FCA restrictions apply to the current user. * @param needApplyFCARestrictions whether FCA restrictions apply to the current user.
* @param state this provider's [SwapState]; price-impact and permission are read from it when it * @param state this provider's [SwapState]; price-impact and permission are read from it when it
* is a [SwapState.QuotesLoadedState]. Null for error rows (which only resolve to FCA / recommended / none). * is a [SwapState.QuotesLoadedState]. Null for error rows (which only resolve to FCA / recommended / none).
* @param isSwapBestDexRateEnabled whether the Best DEX Rate feature toggle is on.
*/ */
fun resolveBadge( fun resolveBadge(
states: Map<SwapProvider, SwapState.QuotesLoadedState>, states: Map<SwapProvider, SwapState.QuotesLoadedState>,
provider: SwapProvider, provider: SwapProvider,
needApplyFCARestrictions: Boolean, needApplyFCARestrictions: Boolean,
state: SwapState? = null, state: SwapState? = null,
isSwapBestDexRateEnabled: Boolean,
): AdditionalBadge { ): AdditionalBadge {
val priceImpact = (state as? SwapState.QuotesLoadedState)?.priceImpact val priceImpact = (state as? SwapState.QuotesLoadedState)?.priceImpact
val permissionState = (state as? SwapState.QuotesLoadedState)?.permissionState val permissionState = (state as? SwapState.QuotesLoadedState)?.permissionState
@ -90,7 +78,7 @@ internal object SwapProviderResolver {
needApplyFCARestrictions && provider.isFCARestricted() -> AdditionalBadge.FCAWarningList needApplyFCARestrictions && provider.isFCARestricted() -> AdditionalBadge.FCAWarningList
permissionState is PermissionDataState.PermissionRequired -> AdditionalBadge.PermissionRequired permissionState is PermissionDataState.PermissionRequired -> AdditionalBadge.PermissionRequired
provider.isRecommended -> AdditionalBadge.Recommended provider.isRecommended -> AdditionalBadge.Recommended
isBestRateBadgeAllowed -> resolveBestRateBadge(states, provider, isSwapBestDexRateEnabled) isBestRateBadgeAllowed -> resolveBestRateBadge(states, provider)
else -> AdditionalBadge.Empty else -> AdditionalBadge.Empty
} }
} }
@ -102,16 +90,10 @@ internal object SwapProviderResolver {
private fun resolveBestRateBadge( private fun resolveBestRateBadge(
states: Map<SwapProvider, SwapState.QuotesLoadedState>, states: Map<SwapProvider, SwapState.QuotesLoadedState>,
provider: SwapProvider, provider: SwapProvider,
isSwapBestDexRateEnabled: Boolean,
): AdditionalBadge { ): AdditionalBadge {
val overallBest = findBestRated(states) val overallBest = findBestRated(states)
val isOverallBest = provider.providerId == overallBest?.providerId val isOverallBest = provider.providerId == overallBest?.providerId
// Toggle off → legacy behaviour: only the overall best provider gets the "Best rate" badge.
if (!isSwapBestDexRateEnabled) {
return if (isOverallBest) AdditionalBadge.BestTrade else AdditionalBadge.Empty
}
val dexStates = states.filterKeys { it.type.isDex() } val dexStates = states.filterKeys { it.type.isDex() }
val hasDex = dexStates.isNotEmpty() val hasDex = dexStates.isNotEmpty()
val hasCex = states.keys.any { !it.type.isDex() } val hasCex = states.keys.any { !it.type.isDex() }

View file

@ -281,7 +281,6 @@ internal class SwapModel @Inject constructor(
get() { get() {
val permissionState = dataState.getCurrentLoadedSwapState()?.permissionState val permissionState = dataState.getCurrentLoadedSwapState()?.permissionState
return permissionState == PermissionDataState.Empty || return permissionState == PermissionDataState.Empty ||
swapFeatureToggles.isSwapIntegratedApproveEnabled &&
permissionState is PermissionDataState.PermissionSettings permissionState is PermissionDataState.PermissionSettings
} }
@ -1175,7 +1174,6 @@ internal class SwapModel @Inject constructor(
needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), needApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
states = loadedStates, states = loadedStates,
state = state, state = state,
isSwapBestDexRateEnabled = swapFeatureToggles.isSwapBestDexRateEnabled,
) )
val swapFee = getSelectedSwapFee() val swapFee = getSelectedSwapFee()
uiState = stateBuilder.createQuotesLoadedState( uiState = stateBuilder.createQuotesLoadedState(
@ -1272,7 +1270,6 @@ internal class SwapModel @Inject constructor(
needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), needApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
states = loadedStates, states = loadedStates,
state = state, state = state,
isSwapBestDexRateEnabled = swapFeatureToggles.isSwapBestDexRateEnabled,
) )
uiState = stateBuilder.createQuotesErrorState( uiState = stateBuilder.createQuotesErrorState(
uiStateHolder = uiState, uiStateHolder = uiState,
@ -1327,10 +1324,7 @@ internal class SwapModel @Inject constructor(
return if (consideredProviders.isNotEmpty()) { return if (consideredProviders.isNotEmpty()) {
val successLoadedData = consideredProviders.getLastLoadedSuccessStates() val successLoadedData = consideredProviders.getLastLoadedSuccessStates()
val bestQuotesProvider = SwapProviderResolver.findBest( val bestQuotesProvider = SwapProviderResolver.findBest(states = successLoadedData)
states = successLoadedData,
isSwapBestDexRateEnabled = swapFeatureToggles.isSwapBestDexRateEnabled,
)
val currentSelected = dataState.selectedProvider val currentSelected = dataState.selectedProvider
if (currentSelected != null && consideredProviders.keys.contains(currentSelected)) { if (currentSelected != null && consideredProviders.keys.contains(currentSelected)) {
// logic for always choose best if already selected provider // logic for always choose best if already selected provider
@ -2111,7 +2105,6 @@ internal class SwapModel @Inject constructor(
selectedProviderId = providerId, selectedProviderId = providerId,
pricesLowerBest = pricesLowerBest, pricesLowerBest = pricesLowerBest,
providersStates = dataState.lastLoadedSwapStates, providersStates = dataState.lastLoadedSwapStates,
isSwapBestDexRateEnabled = swapFeatureToggles.isSwapBestDexRateEnabled,
needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), needApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
) { uiState = stateBuilder.dismissBottomSheet(uiState) } ) { uiState = stateBuilder.dismissBottomSheet(uiState) }
}, },
@ -2663,8 +2656,7 @@ internal class SwapModel @Inject constructor(
val swapDataForCall = resolveDexSwapDataForFee(quoteState) val swapDataForCall = resolveDexSwapDataForFee(quoteState)
.getOrElse { return Either.Left(it) } .getOrElse { return Either.Left(it) }
val integratedSettings = (quoteState.permissionState as? PermissionDataState.PermissionSettings) val integratedSettings = quoteState.permissionState as? PermissionDataState.PermissionSettings
?.takeIf { swapFeatureToggles.isSwapIntegratedApproveEnabled }
return swapInteractor.loadSwapFee( return swapInteractor.loadSwapFee(
quotesLoadedState = quoteState, quotesLoadedState = quoteState,
@ -2833,8 +2825,7 @@ internal class SwapModel @Inject constructor(
private fun isPermissionNotificationShown(): Boolean { private fun isPermissionNotificationShown(): Boolean {
val permissionState = dataState.getCurrentLoadedSwapState()?.permissionState val permissionState = dataState.getCurrentLoadedSwapState()?.permissionState
val isApprovalIntegrated = swapFeatureToggles.isSwapIntegratedApproveEnabled && val isApprovalIntegrated = permissionState is PermissionDataState.PermissionSettings
permissionState is PermissionDataState.PermissionSettings
return permissionState != null && permissionState !is PermissionDataState.Empty && !isApprovalIntegrated return permissionState != null && permissionState !is PermissionDataState.Empty && !isApprovalIntegrated
} }

View file

@ -1220,7 +1220,6 @@ internal class StateBuilder(
pricesLowerBest: Map<String, Float>, pricesLowerBest: Map<String, Float>,
providersStates: Map<SwapProvider, SwapState>, providersStates: Map<SwapProvider, SwapState>,
needApplyFCARestrictions: Boolean, needApplyFCARestrictions: Boolean,
isSwapBestDexRateEnabled: Boolean,
onDismiss: () -> Unit, onDismiss: () -> Unit,
): SwapStateHolder { ): SwapStateHolder {
val successStates = providersStates.getLastLoadedSuccessStates() val successStates = providersStates.getLastLoadedSuccessStates()
@ -1231,7 +1230,6 @@ internal class StateBuilder(
provider = entry.key, provider = entry.key,
needApplyFCARestrictions = needApplyFCARestrictions, needApplyFCARestrictions = needApplyFCARestrictions,
state = entry.value, state = entry.value,
isSwapBestDexRateEnabled = isSwapBestDexRateEnabled,
) )
entry.convertToProviderBottomSheetState( entry.convertToProviderBottomSheetState(
pricesLowerBest = pricesLowerBest, pricesLowerBest = pricesLowerBest,

View file

@ -20,8 +20,8 @@ import java.math.BigDecimal
* row-badge resolution ([SwapProviderResolver.resolveBadge]). * row-badge resolution ([SwapProviderResolver.resolveBadge]).
* *
* Ranking metric: best provider == lowest `from/to` fiat ratio == highest `to` fiat output for the * Ranking metric: best provider == lowest `from/to` fiat ratio == highest `to` fiat output for the
* same `from` input. "Best DEX Rate" prefers the best-rated DEX/DEX_BRIDGE provider when the feature * same `from` input. "Best DEX Rate" prefers the best-rated DEX/DEX_BRIDGE provider when any DEX is
* is on and any DEX is present. * present.
*/ */
internal class SwapProviderResolverTest { internal class SwapProviderResolverTest {
@ -50,7 +50,7 @@ internal class SwapProviderResolverTest {
dexBridge to quote(fromFiat = "100", toFiat = "105"), dexBridge to quote(fromFiat = "100", toFiat = "105"),
) )
val best = SwapProviderResolver.findBest(states, isSwapBestDexRateEnabled = true) val best = SwapProviderResolver.findBest(states)
assertThat(best).isEqualTo(dex1) assertThat(best).isEqualTo(dex1)
} }
@ -62,23 +62,11 @@ internal class SwapProviderResolverTest {
cex2 to quote(fromFiat = "100", toFiat = "120"), // best CEX cex2 to quote(fromFiat = "100", toFiat = "120"), // best CEX
) )
val best = SwapProviderResolver.findBest(states, isSwapBestDexRateEnabled = true) val best = SwapProviderResolver.findBest(states)
assertThat(best).isEqualTo(cex2) assertThat(best).isEqualTo(cex2)
} }
@Test
fun `GIVEN best dex rate off WHEN findBest THEN best overall regardless of type`() {
val states = mapOf(
cex1 to quote(fromFiat = "100", toFiat = "120"), // best overall (a CEX)
dex1 to quote(fromFiat = "100", toFiat = "110"),
)
val best = SwapProviderResolver.findBest(states, isSwapBestDexRateEnabled = false)
assertThat(best).isEqualTo(cex1)
}
@Test @Test
fun `GIVEN DEX_BRIDGE is the best DEX WHEN findBest with best dex rate on THEN DEX_BRIDGE selected`() { fun `GIVEN DEX_BRIDGE is the best DEX WHEN findBest with best dex rate on THEN DEX_BRIDGE selected`() {
val states = mapOf( val states = mapOf(
@ -87,7 +75,7 @@ internal class SwapProviderResolverTest {
dexBridge to quote(fromFiat = "100", toFiat = "115"), // best among DEX-based dexBridge to quote(fromFiat = "100", toFiat = "115"), // best among DEX-based
) )
val best = SwapProviderResolver.findBest(states, isSwapBestDexRateEnabled = true) val best = SwapProviderResolver.findBest(states)
assertThat(best).isEqualTo(dexBridge) assertThat(best).isEqualTo(dexBridge)
} }
@ -110,7 +98,6 @@ internal class SwapProviderResolverTest {
provider = dex1, provider = dex1,
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = states.getValue(dex1), state = states.getValue(dex1),
isSwapBestDexRateEnabled = true,
) )
// Overall best is the DEX → it gets the single "Best rate" badge, NOT "Best DEX rate". // Overall best is the DEX → it gets the single "Best rate" badge, NOT "Best DEX rate".
@ -129,7 +116,6 @@ internal class SwapProviderResolverTest {
provider = cex1, provider = cex1,
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = states.getValue(cex1), state = states.getValue(cex1),
isSwapBestDexRateEnabled = true,
) )
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty) assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
@ -150,7 +136,6 @@ internal class SwapProviderResolverTest {
provider = cex1, provider = cex1,
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = states.getValue(cex1), state = states.getValue(cex1),
isSwapBestDexRateEnabled = true,
) )
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.BestTrade) assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.BestTrade)
@ -169,7 +154,6 @@ internal class SwapProviderResolverTest {
provider = dex1, provider = dex1,
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = states.getValue(dex1), state = states.getValue(dex1),
isSwapBestDexRateEnabled = true,
) )
// CEX wins overall, so the best DEX additionally gets the "Best DEX rate" badge. // CEX wins overall, so the best DEX additionally gets the "Best DEX rate" badge.
@ -189,7 +173,6 @@ internal class SwapProviderResolverTest {
provider = dexBridge, provider = dexBridge,
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = states.getValue(dexBridge), state = states.getValue(dexBridge),
isSwapBestDexRateEnabled = true,
) )
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty) assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
@ -209,7 +192,6 @@ internal class SwapProviderResolverTest {
provider = dex1, provider = dex1,
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = states.getValue(dex1), state = states.getValue(dex1),
isSwapBestDexRateEnabled = true,
) )
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.BestTrade) assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.BestTrade)
@ -227,7 +209,6 @@ internal class SwapProviderResolverTest {
provider = dexBridge, provider = dexBridge,
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = states.getValue(dexBridge), state = states.getValue(dexBridge),
isSwapBestDexRateEnabled = true,
) )
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty) assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
@ -247,51 +228,11 @@ internal class SwapProviderResolverTest {
provider = cex1, provider = cex1,
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = states.getValue(cex1), state = states.getValue(cex1),
isSwapBestDexRateEnabled = true,
) )
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.BestTrade) assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.BestTrade)
} }
// --- Toggle off → only the overall best gets BestTrade; "Best DEX rate" is never produced.
@Test
fun `GIVEN toggle off AND both types present with CEX best WHEN resolveBadge for the CEX THEN BestTrade`() {
val states = mapOf(
cex1 to quote(fromFiat = "100", toFiat = "120"), // best overall
dex1 to quote(fromFiat = "100", toFiat = "110"),
)
val badge = SwapProviderResolver.resolveBadge(
states = states,
provider = cex1,
needApplyFCARestrictions = false,
state = states.getValue(cex1),
isSwapBestDexRateEnabled = false,
)
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.BestTrade)
}
@Test
fun `GIVEN toggle off AND both types present with CEX best WHEN resolveBadge for the DEX THEN Empty`() {
val states = mapOf(
cex1 to quote(fromFiat = "100", toFiat = "120"), // best overall
dex1 to quote(fromFiat = "100", toFiat = "110"),
)
val badge = SwapProviderResolver.resolveBadge(
states = states,
provider = dex1,
needApplyFCARestrictions = false,
state = states.getValue(dex1),
isSwapBestDexRateEnabled = false,
)
// Toggle off → no "Best DEX rate" badge even though a DEX is present and not the overall best.
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
}
@Test @Test
fun `GIVEN FCA restricted provider AND restrictions on WHEN resolveBadge THEN FCAWarningList`() { fun `GIVEN FCA restricted provider AND restrictions on WHEN resolveBadge THEN FCAWarningList`() {
val restricted = provider(id = "changelly", type = ExchangeProviderType.CEX, isRecommended = true) val restricted = provider(id = "changelly", type = ExchangeProviderType.CEX, isRecommended = true)
@ -305,7 +246,6 @@ internal class SwapProviderResolverTest {
provider = restricted, provider = restricted,
needApplyFCARestrictions = true, needApplyFCARestrictions = true,
state = states.getValue(restricted), state = states.getValue(restricted),
isSwapBestDexRateEnabled = true,
) )
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.FCAWarningList) assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.FCAWarningList)
@ -326,7 +266,6 @@ internal class SwapProviderResolverTest {
provider = restricted, provider = restricted,
needApplyFCARestrictions = true, needApplyFCARestrictions = true,
state = states.getValue(restricted), state = states.getValue(restricted),
isSwapBestDexRateEnabled = true,
) )
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.FCAWarningList) assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.FCAWarningList)
@ -346,7 +285,6 @@ internal class SwapProviderResolverTest {
provider = restricted, provider = restricted,
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = states.getValue(restricted), state = states.getValue(restricted),
isSwapBestDexRateEnabled = false,
) )
// Restrictions are off → the restricted id is ignored and the normal best-rate badge wins. // Restrictions are off → the restricted id is ignored and the normal best-rate badge wins.
@ -365,7 +303,6 @@ internal class SwapProviderResolverTest {
provider = cex1, provider = cex1,
needApplyFCARestrictions = true, needApplyFCARestrictions = true,
state = states.getValue(cex1), state = states.getValue(cex1),
isSwapBestDexRateEnabled = true,
) )
// FCA restrictions globally on suppress the best-rate badge even for non-restricted providers. // FCA restrictions globally on suppress the best-rate badge even for non-restricted providers.
@ -388,7 +325,6 @@ internal class SwapProviderResolverTest {
provider = cex1, provider = cex1,
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = states.getValue(cex1), state = states.getValue(cex1),
isSwapBestDexRateEnabled = true,
) )
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.PermissionRequired) assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.PermissionRequired)
@ -407,7 +343,6 @@ internal class SwapProviderResolverTest {
provider = recommended, provider = recommended,
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = states.getValue(recommended), state = states.getValue(recommended),
isSwapBestDexRateEnabled = true,
) )
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Recommended) assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Recommended)
@ -422,7 +357,6 @@ internal class SwapProviderResolverTest {
provider = cex1, provider = cex1,
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = states.getValue(cex1), state = states.getValue(cex1),
isSwapBestDexRateEnabled = true,
) )
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty) assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
@ -440,7 +374,6 @@ internal class SwapProviderResolverTest {
provider = cex2, // not the best provider = cex2, // not the best
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = states.getValue(cex2), state = states.getValue(cex2),
isSwapBestDexRateEnabled = true,
) )
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty) assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
@ -458,7 +391,6 @@ internal class SwapProviderResolverTest {
provider = cex1, provider = cex1,
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = states.getValue(cex1), state = states.getValue(cex1),
isSwapBestDexRateEnabled = true,
) )
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty) assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)
@ -476,7 +408,6 @@ internal class SwapProviderResolverTest {
provider = cex1, provider = cex1,
needApplyFCARestrictions = false, needApplyFCARestrictions = false,
state = null, state = null,
isSwapBestDexRateEnabled = true,
) )
assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty) assertThat(badge).isEqualTo(ProviderState.AdditionalBadge.Empty)

View file

@ -2,7 +2,6 @@ package com.tangem.feature.swap.model
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import com.tangem.feature.swap.domain.models.ui.PermissionDataState import com.tangem.feature.swap.domain.models.ui.PermissionDataState
import io.mockk.every
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -11,7 +10,7 @@ import org.junit.jupiter.api.Test
* *
* The getter is `true` when the current loaded swap state needs no approval: * The getter is `true` when the current loaded swap state needs no approval:
* - always when [PermissionDataState.Empty] * - always when [PermissionDataState.Empty]
* - additionally for [PermissionDataState.PermissionSettings] when the integrated-approve toggle is ON * - additionally for [PermissionDataState.PermissionSettings]
*/ */
internal class SwapModelIsPermissionNotNeededTest : SwapModelTestBase() { internal class SwapModelIsPermissionNotNeededTest : SwapModelTestBase() {
@ -20,11 +19,7 @@ internal class SwapModelIsPermissionNotNeededTest : SwapModelTestBase() {
setUpBase() setUpBase()
} }
private fun modelWithPermissionState( private fun modelWithPermissionState(permissionState: PermissionDataState): SwapModel {
permissionState: PermissionDataState,
isIntegratedApproveEnabled: Boolean,
): SwapModel {
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns isIntegratedApproveEnabled
val provider = swapProvider() val provider = swapProvider()
val model = createModel() val model = createModel()
model.dataState = model.dataState.copy( model.dataState = model.dataState.copy(
@ -35,48 +30,23 @@ internal class SwapModelIsPermissionNotNeededTest : SwapModelTestBase() {
} }
@Test @Test
fun `GIVEN toggle OFF and Empty permission THEN permission is not needed`() { fun `GIVEN Empty permission THEN permission is not needed`() {
val model = modelWithPermissionState(PermissionDataState.Empty, isIntegratedApproveEnabled = false) val model = modelWithPermissionState(PermissionDataState.Empty)
assertThat(model.isPermissionNotNeeded).isTrue() assertThat(model.isPermissionNotNeeded).isTrue()
} }
@Test @Test
fun `GIVEN toggle OFF and PermissionSettings THEN permission is needed`() { fun `GIVEN PermissionSettings THEN permission is not needed`() {
val model = modelWithPermissionState(permissionSettings(), isIntegratedApproveEnabled = false) val model = modelWithPermissionState(permissionSettings())
assertThat(model.isPermissionNotNeeded).isFalse() assertThat(model.isPermissionNotNeeded).isTrue()
} }
@Test @Test
fun `GIVEN toggle OFF and PermissionRequired THEN permission is needed`() { fun `GIVEN PermissionRequired THEN permission is needed`() {
val model = modelWithPermissionState( val model = modelWithPermissionState(
PermissionDataState.PermissionRequired(isResetApproval = false, spenderAddress = "0x"), PermissionDataState.PermissionRequired(isResetApproval = false, spenderAddress = "0x"),
isIntegratedApproveEnabled = false,
)
assertThat(model.isPermissionNotNeeded).isFalse()
}
@Test
fun `GIVEN toggle ON and Empty permission THEN permission is not needed`() {
val model = modelWithPermissionState(PermissionDataState.Empty, isIntegratedApproveEnabled = true)
assertThat(model.isPermissionNotNeeded).isTrue()
}
@Test
fun `GIVEN toggle ON and PermissionSettings THEN permission is not needed`() {
val model = modelWithPermissionState(permissionSettings(), isIntegratedApproveEnabled = true)
assertThat(model.isPermissionNotNeeded).isTrue()
}
@Test
fun `GIVEN toggle ON and PermissionRequired THEN permission is needed`() {
val model = modelWithPermissionState(
PermissionDataState.PermissionRequired(isResetApproval = false, spenderAddress = "0x"),
isIntegratedApproveEnabled = true,
) )
assertThat(model.isPermissionNotNeeded).isFalse() assertThat(model.isPermissionNotNeeded).isFalse()
@ -84,7 +54,6 @@ internal class SwapModelIsPermissionNotNeededTest : SwapModelTestBase() {
@Test @Test
fun `GIVEN no current loaded state THEN permission is needed`() { fun `GIVEN no current loaded state THEN permission is needed`() {
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
val model = createModel() val model = createModel()
assertThat(model.isPermissionNotNeeded).isFalse() assertThat(model.isPermissionNotNeeded).isFalse()

View file

@ -130,7 +130,6 @@ internal abstract class SwapModelTestBase {
coEvery { shouldShowStoriesUseCase.invokeSync(any()) } returns false coEvery { shouldShowStoriesUseCase.invokeSync(any()) } returns false
coEvery { initialCurrenciesResolver.invoke(any(), any(), any(), any()) } returns (null to null) coEvery { initialCurrenciesResolver.invoke(any(), any(), any(), any()) } returns (null to null)
every { getSelectedAppCurrencyUseCase.invoke() } returns emptyFlow() every { getSelectedAppCurrencyUseCase.invoke() } returns emptyFlow()
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
} }
protected fun createParams(): SwapComponent.Params = SwapComponent.Params( protected fun createParams(): SwapComponent.Params = SwapComponent.Params(

View file

@ -2,9 +2,7 @@ package com.tangem.features.tangempay
interface TangemPayFeatureToggles { interface TangemPayFeatureToggles {
val isRedesignEnabled: Boolean val isRedesignEnabled: Boolean
val isCloseCardEnabled: Boolean
val isRemoveAccountEnabled: Boolean val isRemoveAccountEnabled: Boolean
val isMultipleCardsEnabled: Boolean
val isTiersPlusPlanEnabled: Boolean val isTiersPlusPlanEnabled: Boolean
val isCashbackEnabled: Boolean val isCashbackEnabled: Boolean
} }

View file

@ -7,18 +7,11 @@ internal class DefaultTangemPayFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager, private val featureTogglesManager: FeatureTogglesManager,
) : TangemPayFeatureToggles { ) : TangemPayFeatureToggles {
override val isRedesignEnabled: Boolean override val isRedesignEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15368_VISA_PAY_REDESIGN) && get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.APP_REDESIGN_ENABLED)
featureTogglesManager.isFeatureEnabled(FeatureToggles.APP_REDESIGN_ENABLED)
override val isCloseCardEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15364_VISA_PAY_CARD_CLOSE)
override val isRemoveAccountEnabled: Boolean override val isRemoveAccountEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15741_VISA_PAY_REMOVE_ACCOUNT) get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15741_VISA_PAY_REMOVE_ACCOUNT)
override val isMultipleCardsEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15235_VISA_MULTIPLE_CARDS)
override val isTiersPlusPlanEnabled: Boolean override val isTiersPlusPlanEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_16041_VISA_TIERS_PLUS_PLAN) get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_16041_VISA_TIERS_PLUS_PLAN)

View file

@ -35,7 +35,6 @@ internal class TangemPayDetailsStateFactory(
private val intents: TangemPayDetailIntents, private val intents: TangemPayDetailIntents,
private val isRedesignEnabled: Boolean, private val isRedesignEnabled: Boolean,
private val isRemoveAccountEnabled: Boolean, private val isRemoveAccountEnabled: Boolean,
private val isMultipleCardsEnabled: Boolean,
private val isTiersPlusPlanEnabled: Boolean, private val isTiersPlusPlanEnabled: Boolean,
) { ) {
private val notificationFactory = TangemPayDetailsNotificationFactory( private val notificationFactory = TangemPayDetailsNotificationFactory(
@ -100,7 +99,6 @@ internal class TangemPayDetailsStateFactory(
), ),
cardsBlockState = TangemPayDetailsBalanceBlockState.CardsBlockState( cardsBlockState = TangemPayDetailsBalanceBlockState.CardsBlockState(
cards = status.cards cards = status.cards
.let { if (isMultipleCardsEnabled) it else it.take(1) }
.map { cardItem -> .map { cardItem ->
TangemPayDetailsBalanceBlockState.Card( TangemPayDetailsBalanceBlockState.Card(
lastDigits = cardItem.lastDigits, lastDigits = cardItem.lastDigits,

View file

@ -110,7 +110,6 @@ internal class TangemPayCardPageModel @Inject constructor(
private val cryptoCurrency private val cryptoCurrency
get() = currentStatus.value.cryptoCurrency get() = currentStatus.value.cryptoCurrency
private val isMultipleCardsEnabled: Boolean get() = tangemPayFeatureToggles.isMultipleCardsEnabled
val uiState: StateFlow<TangemPayCardPageUM> val uiState: StateFlow<TangemPayCardPageUM>
field = MutableStateFlow( field = MutableStateFlow(
@ -185,7 +184,7 @@ internal class TangemPayCardPageModel @Inject constructor(
val status = state.value val status = state.value
if (status !is PaymentAccountStatusValue.Loaded || status.source != StatusSource.ACTUAL) return if (status !is PaymentAccountStatusValue.Loaded || status.source != StatusSource.ACTUAL) return
val cards = status.cards.let { if (isMultipleCardsEnabled) it else it.take(1) } val cards = status.cards
val newIds = cards.mapTo(mutableSetOf()) { it.id } val newIds = cards.mapTo(mutableSetOf()) { it.id }
cardControllers.keys.filterNot { it in newIds }.toList().forEach { removedId -> cardControllers.keys.filterNot { it in newIds }.toList().forEach { removedId ->
@ -334,30 +333,28 @@ internal class TangemPayCardPageModel @Inject constructor(
), ),
), ),
) )
if (tangemPayFeatureToggles.isCloseCardEnabled) { add(
add( TangemPayDropDownItemUM(
TangemPayDropDownItemUM( title = TextReference.Res(R.string.tangem_pay_close_card_popup_primary_button_title),
title = TextReference.Res(R.string.tangem_pay_close_card_popup_primary_button_title), onClick = ::onClickCloseCard,
onClick = ::onClickCloseCard, icon = TangemIconUM.Icon(
icon = TangemIconUM.Icon( iconRes = CoreUiR.drawable.ic_trash_24,
iconRes = CoreUiR.drawable.ic_trash_24, tintReference = {
tintReference = { if (isLastCard) {
if (isLastCard) { TangemTheme.colors3.icon.tertiary
TangemTheme.colors3.icon.tertiary } else {
} else { TangemTheme.colors3.icon.primary
TangemTheme.colors3.icon.primary }
}
},
),
subtitle = if (isLastCard) {
TextReference.Res(R.string.tangem_pay_close_card_disabled_last_card)
} else {
null
}, },
isEnabled = !isLastCard,
), ),
) subtitle = if (isLastCard) {
} TextReference.Res(R.string.tangem_pay_close_card_disabled_last_card)
} else {
null
},
isEnabled = !isLastCard,
),
)
}.toImmutableList() }.toImmutableList()
} }

View file

@ -111,15 +111,12 @@ internal class TangemPayDetailsModel @Inject constructor(
val cryptoCurrency val cryptoCurrency
get() = currentStatus.value.cryptoCurrency get() = currentStatus.value.cryptoCurrency
private val isMultipleCardsEnabled: Boolean get() = tangemPayFeatureToggles.isMultipleCardsEnabled
private val stateFactory = TangemPayDetailsStateFactory( private val stateFactory = TangemPayDetailsStateFactory(
onBack = router::pop, onBack = router::pop,
onOpenMenu = ::onOpenMenu, onOpenMenu = ::onOpenMenu,
intents = this, intents = this,
isRedesignEnabled = isRedesignEnabled(), isRedesignEnabled = isRedesignEnabled(),
isRemoveAccountEnabled = tangemPayFeatureToggles.isRemoveAccountEnabled, isRemoveAccountEnabled = tangemPayFeatureToggles.isRemoveAccountEnabled,
isMultipleCardsEnabled = isMultipleCardsEnabled,
isTiersPlusPlanEnabled = tangemPayFeatureToggles.isTiersPlusPlanEnabled, isTiersPlusPlanEnabled = tangemPayFeatureToggles.isTiersPlusPlanEnabled,
) )
@ -476,17 +473,6 @@ internal class TangemPayDetailsModel @Inject constructor(
override fun onAddCardClick() { override fun onAddCardClick() {
analytics.send(TangemPayAnalyticsEvents.AddExtraCardClicked()) analytics.send(TangemPayAnalyticsEvents.AddExtraCardClicked())
if (!isMultipleCardsEnabled) {
analytics.send(TangemPayAnalyticsEvents.FakeDoorPopupDisplayed())
uiMessageSender.send(
TangemPayMessagesFactory.createFutureFeature(
onGotItClick = {
analytics.send(TangemPayAnalyticsEvents.FakeDoorGotitClicked())
},
),
)
return
}
modelScope.launch { modelScope.launch {
val offer = getCustomerOffers.additionalCardOffer(userWalletId).getOrNull() val offer = getCustomerOffers.additionalCardOffer(userWalletId).getOrNull()
if (offer == null) { if (offer == null) {

View file

@ -31,7 +31,6 @@ internal class TangemPayDetailsStateFactoryTest {
intents = intents, intents = intents,
isRedesignEnabled = true, isRedesignEnabled = true,
isRemoveAccountEnabled = true, isRemoveAccountEnabled = true,
isMultipleCardsEnabled = true,
isTiersPlusPlanEnabled = true, isTiersPlusPlanEnabled = true,
) )

View file

@ -1,6 +0,0 @@
package com.tangem.features.tokendetails
interface TokenDetailsFeatureToggles {
val isQuickTopUpEnabled: Boolean
val isManageFundsEnabled: Boolean
}

View file

@ -1,18 +0,0 @@
package com.tangem.feature.tokendetails
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.tokendetails.TokenDetailsFeatureToggles
import javax.inject.Inject
internal class DefaultTokenDetailsFeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) : TokenDetailsFeatureToggles {
override val isQuickTopUpEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_15258_QUICK_TOP_UP_ENABLED,
)
override val isManageFundsEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1377_MANAGE_FUNDS)
}

View file

@ -1,21 +0,0 @@
package com.tangem.feature.tokendetails.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.feature.tokendetails.DefaultTokenDetailsFeatureToggles
import com.tangem.features.tokendetails.TokenDetailsFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object TokenDetailsFeatureModule {
@Provides
@Singleton
fun provideTokenDetailsFeatureToggles(featureTogglesManager: FeatureTogglesManager): TokenDetailsFeatureToggles {
return DefaultTokenDetailsFeatureToggles(featureTogglesManager)
}
}

View file

@ -8,15 +8,12 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.onramp.model.OnrampAvailability import com.tangem.domain.onramp.model.OnrampAvailability
import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.feature.tokendetails.presentation.tokendetails.state.QuickTopUpBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.QuickTopUpBlockUM
import com.tangem.features.tokendetails.TokenDetailsFeatureToggles
import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.isZero
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal import java.math.BigDecimal
import javax.inject.Inject import javax.inject.Inject
internal class QuickTopUpBlockFactory @Inject constructor( internal class QuickTopUpBlockFactory @Inject constructor() {
private val featureToggles: TokenDetailsFeatureToggles,
) {
fun build( fun build(
currencyStatus: CryptoCurrencyStatus, currencyStatus: CryptoCurrencyStatus,
@ -25,8 +22,6 @@ internal class QuickTopUpBlockFactory @Inject constructor(
onPresetClick: (BigDecimal, String) -> Unit, onPresetClick: (BigDecimal, String) -> Unit,
onOtherClick: () -> Unit, onOtherClick: () -> Unit,
): QuickTopUpBlockUM? { ): QuickTopUpBlockUM? {
if (!featureToggles.isQuickTopUpEnabled) return null
val amount = currencyStatus.value.amount val amount = currencyStatus.value.amount
if (amount == null || !amount.isZero()) return null if (amount == null || !amount.isZero()) return null

View file

@ -20,7 +20,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTr
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.utils.Provider import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted import dagger.assisted.Assisted
@ -47,7 +46,6 @@ internal class ExpressStatusFactory @AssistedInject constructor(
private val analyticsEventsHandler: AnalyticsEventHandler, private val analyticsEventsHandler: AnalyticsEventHandler,
onrampStatusFactory: OnrampStatusFactory.Factory, onrampStatusFactory: OnrampStatusFactory.Factory,
exchangeStatusFactory: ExchangeStatusFactory.Factory, exchangeStatusFactory: ExchangeStatusFactory.Factory,
private val swapFeatureToggles: SwapFeatureToggles,
private val txHistoryFeatureToggles: TxHistoryFeatureToggles, private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
) { ) {
@ -221,7 +219,6 @@ internal class ExpressStatusFactory @AssistedInject constructor(
is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet( is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(
config = this, config = this,
extraContent = extraContent, extraContent = extraContent,
isExpressShareButtonEnabled = swapFeatureToggles.isExpressShareButtonEnabled,
) )
} }
} }

View file

@ -21,44 +21,28 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.e
@Composable @Composable
internal fun ExpressStatusBottomSheet( internal fun ExpressStatusBottomSheet(
config: TangemBottomSheetConfig, config: TangemBottomSheetConfig,
isExpressShareButtonEnabled: Boolean,
extraContent: (@Composable () -> Unit)? = null, extraContent: (@Composable () -> Unit)? = null,
) { ) {
TangemBottomSheet( TangemBottomSheet(
config = config, config = config,
containerColor = TangemTheme.colors.background.tertiary, containerColor = TangemTheme.colors.background.tertiary,
) { content: ExpressStatusBottomSheetConfig -> ) { content: ExpressStatusBottomSheetConfig ->
if (isExpressShareButtonEnabled) { Box {
Box {
when (val state = content.value) {
is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(
state = state,
isExpressShareButtonEnabled = true,
)
is ExchangeUM -> ExchangeStatusBottomSheetContent(
state = state,
extraContent = extraContent,
isExpressShareButtonEnabled = true,
)
}
BottomFade(
backgroundColor = TangemTheme.colors.background.tertiary,
modifier = Modifier.align(Alignment.BottomCenter),
)
ExpressShareContent(state = content.value)
}
} else {
when (val state = content.value) { when (val state = content.value) {
is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent( is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(
state = state, state = state,
isExpressShareButtonEnabled = false, isExpressShareButtonEnabled = true,
) )
is ExchangeUM -> ExchangeStatusBottomSheetContent( is ExchangeUM -> ExchangeStatusBottomSheetContent(
state = state, state = state,
extraContent = extraContent, extraContent = extraContent,
isExpressShareButtonEnabled = false,
) )
} }
BottomFade(
backgroundColor = TangemTheme.colors.background.tertiary,
modifier = Modifier.align(Alignment.BottomCenter),
)
ExpressShareContent(state = content.value)
} }
} }
} }
@ -76,7 +60,6 @@ private fun PreviewExpressStatusBottomSheet(
onDismissRequest = {}, onDismissRequest = {},
content = param, content = param,
), ),
isExpressShareButtonEnabled = false,
) )
} }
} }

View file

@ -25,7 +25,6 @@ import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH10 import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.notifications.CurrencyNotification import com.tangem.core.ui.components.notifications.CurrencyNotification
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
@ -38,11 +37,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.E
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
internal fun ExchangeStatusBottomSheetContent( internal fun ExchangeStatusBottomSheetContent(state: ExchangeUM, extraContent: (@Composable () -> Unit)? = null) {
state: ExchangeUM,
isExpressShareButtonEnabled: Boolean,
extraContent: (@Composable () -> Unit)? = null,
) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16) .padding(horizontal = TangemTheme.dimens.spacing16)
@ -97,11 +92,7 @@ internal fun ExchangeStatusBottomSheetContent(
isAutoDisposable = state.activeStatus?.isAutoDisposable == true, isAutoDisposable = state.activeStatus?.isAutoDisposable == true,
onClick = state.info.onDisposeExpressStatus, onClick = state.info.onDisposeExpressStatus,
) )
if (isExpressShareButtonEnabled) { SpacerH(80.dp)
SpacerH(80.dp)
} else {
SpacerH24()
}
} }
} }

View file

@ -11,7 +11,6 @@ import com.tangem.domain.onramp.model.OnrampAvailability
import com.tangem.domain.onramp.model.OnrampCountry import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.features.tokendetails.TokenDetailsFeatureToggles
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -19,10 +18,7 @@ import java.math.BigDecimal
internal class QuickTopUpBlockFactoryTest { internal class QuickTopUpBlockFactoryTest {
private val featureToggles: TokenDetailsFeatureToggles = mockk { private val factory = QuickTopUpBlockFactory()
every { isQuickTopUpEnabled } returns true
}
private val factory = QuickTopUpBlockFactory(featureToggles)
private val zeroBalanceStatus: CryptoCurrencyStatus = mockk { private val zeroBalanceStatus: CryptoCurrencyStatus = mockk {
every { value } returns mockk<CryptoCurrencyStatus.Loaded> { every { value } returns mockk<CryptoCurrencyStatus.Loaded> {
@ -69,24 +65,6 @@ internal class QuickTopUpBlockFactoryTest {
private val notSupported: OnrampAvailability = OnrampAvailability.NotSupported(country = countryMock) private val notSupported: OnrampAvailability = OnrampAvailability.NotSupported(country = countryMock)
@Test
fun `returns null when feature toggle is disabled`() {
val disabledToggles: TokenDetailsFeatureToggles = mockk {
every { isQuickTopUpEnabled } returns false
}
val disabledFactory = QuickTopUpBlockFactory(disabledToggles)
val result = disabledFactory.build(
currencyStatus = zeroBalanceStatus,
isHistoryEmpty = true,
onrampAvailability = availableUsd.right(),
onPresetClick = { _, _ -> },
onOtherClick = {},
)
assertThat(result).isNull()
}
@Test @Test
fun `returns null when balance is non-zero`() { fun `returns null when balance is non-zero`() {
val result = factory.build( val result = factory.build(

View file

@ -1,11 +0,0 @@
package com.tangem.features.wallet.featuretoggles
/**
* Wallet feature toggles
*
[REDACTED_AUTHOR]
*/
interface WalletFeatureToggles {
val isManageFundsEnabled: Boolean
}

View file

@ -67,7 +67,6 @@ import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
import com.tangem.features.pushnotificationsettings.PushNotificationSettingsFeatureToggles import com.tangem.features.pushnotificationsettings.PushNotificationSettingsFeatureToggles
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.utils.Provider import com.tangem.utils.Provider
import com.tangem.utils.coroutines.* import com.tangem.utils.coroutines.*
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
@ -125,7 +124,6 @@ internal class WalletModel @Inject constructor(
private val resolveQrSendTargetsUseCase: ResolveQrSendTargetsUseCase, private val resolveQrSendTargetsUseCase: ResolveQrSendTargetsUseCase,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
private val uiMessageSender: UiMessageSender, private val uiMessageSender: UiMessageSender,
private val walletFeatureToggles: WalletFeatureToggles,
private val pushNotificationSettingsFeatureToggles: PushNotificationSettingsFeatureToggles, private val pushNotificationSettingsFeatureToggles: PushNotificationSettingsFeatureToggles,
private val addressBookFeatureToggles: AddressBookFeatureToggles, private val addressBookFeatureToggles: AddressBookFeatureToggles,
private val startAssetsDiscoveryUseCase: StartAssetsDiscoveryUseCase, private val startAssetsDiscoveryUseCase: StartAssetsDiscoveryUseCase,
@ -577,7 +575,6 @@ internal class WalletModel @Inject constructor(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled,
), ),
) )
@ -624,7 +621,6 @@ internal class WalletModel @Inject constructor(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled,
), ),
) )
} }
@ -646,7 +642,6 @@ internal class WalletModel @Inject constructor(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled,
), ),
) )
} }
@ -663,7 +658,6 @@ internal class WalletModel @Inject constructor(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled,
), ),
) )
@ -725,7 +719,6 @@ internal class WalletModel @Inject constructor(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled,
), ),
) )

View file

@ -1,18 +0,0 @@
package com.tangem.feature.wallet.di
import com.tangem.feature.wallet.featuretoggles.DefaultWalletFeatureToggles
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface WalletFeatureTogglesModule {
@Singleton
@Binds
fun bindWalletFeatureToggles(toggles: DefaultWalletFeatureToggles): WalletFeatureToggles
}

View file

@ -1,14 +0,0 @@
package com.tangem.feature.wallet.featuretoggles
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import javax.inject.Inject
internal class DefaultWalletFeatureToggles @Inject constructor(
private val featureToggles: FeatureTogglesManager,
) : WalletFeatureToggles {
override val isManageFundsEnabled: Boolean
get() = featureToggles.isFeatureEnabled(FeatureToggles.TWI_1377_MANAGE_FUNDS)
}

View file

@ -13,7 +13,6 @@ internal class AddWalletTransformer(
private val clickIntents: WalletClickIntents, private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver, private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase, private val getWalletIconUseCase: GetWalletIconUseCase,
private val isManageFundsEnabled: Boolean,
) : WalletScreenStateTransformer { ) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy { private val walletLoadingStateFactory by lazy {
@ -21,7 +20,6 @@ internal class AddWalletTransformer(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isManageFundsEnabled = isManageFundsEnabled,
) )
} }

View file

@ -28,7 +28,6 @@ internal class InitializeWalletsTransformer(
private val clickIntents: WalletClickIntents, private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver, private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase, private val getWalletIconUseCase: GetWalletIconUseCase,
private val isManageFundsEnabled: Boolean,
) : WalletScreenStateTransformer { ) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy { private val walletLoadingStateFactory by lazy {
@ -36,7 +35,6 @@ internal class InitializeWalletsTransformer(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isManageFundsEnabled = isManageFundsEnabled,
) )
} }
@ -156,11 +154,7 @@ internal class InitializeWalletsTransformer(
onClick = {}, onClick = {},
) )
val lastButton = if (isManageFundsEnabled) { val lastButton = WalletManageButton.Transfer(enabled = false, dimContent = false, onClick = {})
WalletManageButton.Transfer(enabled = false, dimContent = false, onClick = {})
} else {
WalletManageButton.Sell(enabled = false, dimContent = false, onClick = {})
}
return persistentListOf( return persistentListOf(
firstButton, firstButton,
@ -193,21 +187,12 @@ internal class InitializeWalletsTransformer(
onClick = {}, onClick = {},
).buttonUM, ).buttonUM,
) )
if (isManageFundsEnabled) { add(
add( WalletActionButtons.Transfer(
WalletActionButtons.Transfer( isEnabled = false,
isEnabled = false, onClick = {},
onClick = {}, ).buttonUM,
).buttonUM, )
)
} else {
add(
WalletActionButtons.Sell(
isEnabled = false,
onClick = {},
).buttonUM,
)
}
}.toPersistentList() }.toPersistentList()
} }
} }

View file

@ -25,7 +25,6 @@ internal class ReinitializeNewWalletTransformer(
private val clickIntents: WalletClickIntents, private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver, private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase, private val getWalletIconUseCase: GetWalletIconUseCase,
private val isManageFundsEnabled: Boolean,
) : WalletScreenStateTransformer { ) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy { private val walletLoadingStateFactory by lazy {
@ -33,7 +32,6 @@ internal class ReinitializeNewWalletTransformer(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isManageFundsEnabled = isManageFundsEnabled,
) )
} }

View file

@ -20,7 +20,6 @@ internal class ReinitializeWalletTransformer(
private val clickIntents: WalletClickIntents, private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver, private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase, private val getWalletIconUseCase: GetWalletIconUseCase,
private val isManageFundsEnabled: Boolean,
) : WalletStateTransformer(userWalletId = userWallet.walletId) { ) : WalletStateTransformer(userWalletId = userWallet.walletId) {
private val walletLoadingStateFactory by lazy { private val walletLoadingStateFactory by lazy {
@ -28,7 +27,6 @@ internal class ReinitializeWalletTransformer(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isManageFundsEnabled = isManageFundsEnabled,
) )
} }

View file

@ -18,7 +18,6 @@ internal class UnlockWalletTransformer(
private val clickIntents: WalletClickIntents, private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver, private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase, private val getWalletIconUseCase: GetWalletIconUseCase,
private val isManageFundsEnabled: Boolean,
) : WalletScreenStateTransformer { ) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy { private val walletLoadingStateFactory by lazy {
@ -26,7 +25,6 @@ internal class UnlockWalletTransformer(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isManageFundsEnabled = isManageFundsEnabled,
) )
} }

View file

@ -34,7 +34,6 @@ internal class WalletLoadingStateFactory(
private val clickIntents: WalletClickIntents, private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver, private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase, private val getWalletIconUseCase: GetWalletIconUseCase,
private val isManageFundsEnabled: Boolean,
) { ) {
fun create(userWallet: UserWallet): WalletState { fun create(userWallet: UserWallet): WalletState {
@ -157,19 +156,11 @@ internal class WalletLoadingStateFactory(
onClick = { clickIntents.onAddFundsClick(userWallet.walletId) }, onClick = { clickIntents.onAddFundsClick(userWallet.walletId) },
) )
val lastButton = if (isManageFundsEnabled) { val lastButton = WalletManageButton.Transfer(
WalletManageButton.Transfer( enabled = true,
enabled = true, dimContent = false,
dimContent = false, onClick = { clickIntents.onTransferClick(userWallet.walletId) },
onClick = { clickIntents.onTransferClick(userWallet.walletId) }, )
)
} else {
WalletManageButton.Sell(
enabled = true,
dimContent = false,
onClick = { clickIntents.onMultiWalletSellClick(userWalletId = userWallet.walletId) },
)
}
return persistentListOf( return persistentListOf(
firstButton, firstButton,
@ -201,25 +192,14 @@ internal class WalletLoadingStateFactory(
}, },
).buttonUM, ).buttonUM,
) )
if (isManageFundsEnabled) { add(
add( WalletActionButtons.Transfer(
WalletActionButtons.Transfer( isEnabled = false,
isEnabled = false, onClick = {
onClick = { clickIntents.onTransferClick(userWalletId = userWallet.walletId)
clickIntents.onTransferClick(userWalletId = userWallet.walletId) },
}, ).buttonUM,
).buttonUM, )
)
} else {
add(
WalletActionButtons.Sell(
isEnabled = false,
onClick = {
clickIntents.onMultiWalletSellClick(userWalletId = userWallet.walletId)
},
).buttonUM,
)
}
}.toPersistentList() }.toPersistentList()
} }

View file

@ -13,7 +13,6 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.features.polymarket.api.PolymarketFeatureToggles import com.tangem.features.polymarket.api.PolymarketFeatureToggles
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.utils.coroutines.combine7 import com.tangem.utils.coroutines.combine7
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted import dagger.assisted.Assisted
@ -39,7 +38,6 @@ internal class AccountListSubscriber @AssistedInject constructor(
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
private val designFeatureToggles: DesignFeatureToggles, private val designFeatureToggles: DesignFeatureToggles,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val polymarketFeatureToggles: PolymarketFeatureToggles, private val polymarketFeatureToggles: PolymarketFeatureToggles,
) : BasicAccountListSubscriber() { ) : BasicAccountListSubscriber() {
@ -94,7 +92,7 @@ internal class AccountListSubscriber @AssistedInject constructor(
yieldSupplyApyMap = yieldSupplyApyMap, yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap, stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo, shouldShowMainPromo = shouldShowMainPromo,
isMultipleCardsEnabled = tangemPayFeatureToggles.isMultipleCardsEnabled, isMultipleCardsEnabled = true,
isPolymarketEnabled = polymarketFeatureToggles.isPolymarketEnabled, isPolymarketEnabled = polymarketFeatureToggles.isPolymarketEnabled,
) )
} else { } else {

View file

@ -5,7 +5,6 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.features.tangempay.TangemPayFeatureToggles
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -21,7 +20,6 @@ internal class SingleWalletSubscriber @AssistedInject constructor(
override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
override val stateController: WalletStateController, override val stateController: WalletStateController,
override val clickIntents: WalletClickIntents, override val clickIntents: WalletClickIntents,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
) : BasicAccountListSubscriber() { ) : BasicAccountListSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<Unit> = combine( override fun create(coroutineScope: CoroutineScope): Flow<Unit> = combine(
@ -29,7 +27,7 @@ internal class SingleWalletSubscriber @AssistedInject constructor(
flow2 = getAppCurrencyFlow(), flow2 = getAppCurrencyFlow(),
flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet), flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet),
flow4 = accountDependencies.isAccountsModeEnabledUseCase(), flow4 = accountDependencies.isAccountsModeEnabledUseCase(),
flow5 = flowOf(tangemPayFeatureToggles.isMultipleCardsEnabled), flow5 = flowOf(true),
transform = ::updateState2, transform = ::updateState2,
) )

View file

@ -42,7 +42,7 @@ internal class WalletManagerFactoryCreator @Inject constructor(
isSolanaTxHistoryEnabled = true, isSolanaTxHistoryEnabled = true,
isSolanaScaledUiAmountEnabled = true, isSolanaScaledUiAmountEnabled = true,
isHederaErc20Enabled = true, isHederaErc20Enabled = true,
isStateOverrideGasEstimateEnabled = featureToggleValues.isStateOverrideGasEstimateEnabled, isStateOverrideGasEstimateEnabled = true,
), ),
blockchainDataStorage = blockchainDataStorage, blockchainDataStorage = blockchainDataStorage,
loggers = listOf(blockchainSDKLogger), loggers = listOf(blockchainSDKLogger),
@ -51,6 +51,5 @@ internal class WalletManagerFactoryCreator @Inject constructor(
data class FeatureToggleValues( data class FeatureToggleValues(
val isYieldModeSwapEnabled: Boolean, val isYieldModeSwapEnabled: Boolean,
val isStateOverrideGasEstimateEnabled: Boolean,
) )
} }

View file

@ -101,9 +101,6 @@ internal object BlockchainSDKFactoryModule {
isYieldModeSwapEnabled = featureTogglesManager.isFeatureEnabled( isYieldModeSwapEnabled = featureTogglesManager.isFeatureEnabled(
FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED, FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED,
), ),
isStateOverrideGasEstimateEnabled = featureTogglesManager.isFeatureEnabled(
FeatureToggles.AND_15120_SWAP_INTEGRATED_APPROVE,
),
), ),
) )
} }