From b5209c613d1facba2a503bc468e6cdb0f3377a11 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Jun 2026 15:39:11 +0300 Subject: [PATCH] Updated on 2026-08-14 --- ...eldBoostPromoEnabledForTokenUseCaseTest.kt | 15 +++++ ...ouldShowYieldBoostMainBannerUseCaseTest.kt | 15 +++++ .../tokendetails/model/TokenDetailsModel.kt | 14 +++-- .../UpdateTopBarMenuTransformer.kt | 22 +++++-- .../UpdateTopBarMenuTransformerTest.kt | 63 ++++++++++++++++--- 5 files changed, 108 insertions(+), 21 deletions(-) diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/IsYieldBoostPromoEnabledForTokenUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/IsYieldBoostPromoEnabledForTokenUseCaseTest.kt index 9afbbb6492..f8f9929dd0 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/IsYieldBoostPromoEnabledForTokenUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/IsYieldBoostPromoEnabledForTokenUseCaseTest.kt @@ -7,11 +7,14 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.yield.supply.models.YieldBoostPromo import com.tangem.domain.yield.supply.models.YieldBoostStatus import com.tangem.domain.yield.supply.promo.YieldPromoRepository +import io.mockk.Deregisterable import io.mockk.coEvery import io.mockk.mockk +import io.mockk.registerInstanceFactory import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -25,11 +28,23 @@ class IsYieldBoostPromoEnabledForTokenUseCaseTest { private val contractAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" private val networkRawId = "ethereum" + // Stub concrete instances of the sealed return types so MockK doesn't subclass them while recording coEvery + // (Objenesis on a JVM-sealed type throws InstantiationError flakily under full-suite CI runs). + private val instanceFactories = mutableListOf() + @BeforeEach fun setUp() { + instanceFactories += registerInstanceFactory { YieldBoostPromo.None } + instanceFactories += registerInstanceFactory { YieldBoostStatus.NotStarted } useCase = IsYieldBoostPromoEnabledForTokenUseCase(repository = repository) } + @AfterEach + fun tearDown() { + instanceFactories.forEach { it.deregister() } + instanceFactories.clear() + } + @Test fun `GIVEN currency is coin WHEN invoke THEN returns Right(false)`() = runTest { val coin = createCoin() diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/ShouldShowYieldBoostMainBannerUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/ShouldShowYieldBoostMainBannerUseCaseTest.kt index ae20808d28..72e4c6e0fa 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/ShouldShowYieldBoostMainBannerUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/ShouldShowYieldBoostMainBannerUseCaseTest.kt @@ -5,11 +5,14 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.yield.supply.models.YieldBoostPromo import com.tangem.domain.yield.supply.models.YieldBoostStatus import com.tangem.domain.yield.supply.promo.YieldPromoRepository +import io.mockk.Deregisterable import io.mockk.coEvery import io.mockk.mockk +import io.mockk.registerInstanceFactory import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -23,11 +26,23 @@ class ShouldShowYieldBoostMainBannerUseCaseTest { private val contractAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" private val networkRawId = "ethereum" + // Stub concrete instances of the sealed return types so MockK doesn't subclass them while recording coEvery + // (Objenesis on a JVM-sealed type throws InstantiationError flakily under full-suite CI runs). + private val instanceFactories = mutableListOf() + @BeforeEach fun setUp() { + instanceFactories += registerInstanceFactory { YieldBoostPromo.None } + instanceFactories += registerInstanceFactory { YieldBoostStatus.NotStarted } useCase = ShouldShowYieldBoostMainBannerUseCase(repository = repository) } + @AfterEach + fun tearDown() { + instanceFactories.forEach { it.deregister() } + instanceFactories.clear() + } + @Test fun `GIVEN promo repository throws WHEN invoke THEN returns Left`() = runTest { coEvery { repository.getYieldBoostPromo(userWalletId, false) } throws RuntimeException("net") diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index bcbc740e09..df605c6c7e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -533,7 +533,7 @@ internal class TokenDetailsModel @Inject constructor( network = cryptoCurrency.network, ).getOrElse { false } - val isSupported = isXPUBSupported() + val isSupported = isXpubSupported() val isDynamicAddressesAvailable = isSupported && isDynamicAddressesAvailableUseCase(userWallet, cryptoCurrency) @@ -546,7 +546,7 @@ internal class TokenDetailsModel @Inject constructor( } } - private suspend fun isXPUBSupported(): Boolean { + private suspend fun isXpubSupported(): Boolean { return isXpubSupportedUseCase(userWalletId = userWalletId, network = cryptoCurrency.network) } @@ -1437,15 +1437,17 @@ internal class TokenDetailsModel @Inject constructor( network = cryptoCurrency.network, ).getOrElse { false } - val isSupported = isXPUBSupported() + val isXpubSupported = isXpubSupported() + val isDynamicAddressesAvailable = isXpubSupported && + isDynamicAddressesAvailableUseCase(userWallet, cryptoCurrency) redesignStateController.update( UpdateTopBarMenuTransformer( userWallet = userWallet, hasDerivations = hasDerivations, - isXPubSupported = isSupported, - onGenerateExtendedKey = ::onGenerateExtendedKey, - onHideClick = ::onHideClick, + isXpubSupported = isXpubSupported, + isDynamicAddressesAvailable = isDynamicAddressesAvailable, + clickIntents = this@TokenDetailsModel, ), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt index 320e28bdce..682927afcf 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.themedColor import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM import com.tangem.features.tokendetails.impl.R import com.tangem.utils.transformer.Transformer @@ -15,9 +16,9 @@ import kotlinx.collections.immutable.toImmutableList internal class UpdateTopBarMenuTransformer( private val userWallet: UserWallet, private val hasDerivations: Boolean, - private val isXPubSupported: Boolean, - private val onGenerateExtendedKey: () -> Unit, - private val onHideClick: () -> Unit, + private val isXpubSupported: Boolean, + private val isDynamicAddressesAvailable: Boolean, + private val clickIntents: TokenDetailsClickIntents, ) : Transformer { override fun transform(prevState: TokenDetailsUM): TokenDetailsUM = prevState.copy( @@ -30,12 +31,21 @@ internal class UpdateTopBarMenuTransformer( persistentListOf() } else { buildList { - if (isXPubSupported && hasDerivations) { + if (isDynamicAddressesAvailable) { + add( + TangemDropdownMenuItem( + title = resourceReference(R.string.dynamic_addresses), + textColor = themedColor { TangemTheme.colors.text.primary1 }, + onClick = clickIntents::onDynamicAddressesClick, + ), + ) + } + if (isXpubSupported && hasDerivations) { add( TangemDropdownMenuItem( title = resourceReference(R.string.token_details_generate_xpub), textColor = themedColor { TangemTheme.colors.text.primary1 }, - onClick = onGenerateExtendedKey, + onClick = clickIntents::onGenerateExtendedKey, ), ) } @@ -43,7 +53,7 @@ internal class UpdateTopBarMenuTransformer( TangemDropdownMenuItem( title = resourceReference(R.string.token_details_hide_token), textColor = themedColor { TangemTheme.colors.text.warning }, - onClick = onHideClick, + onClick = clickIntents::onHideClick, ), ) }.toImmutableList() diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt index d66631ccae..3c53e0f255 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM @@ -29,8 +30,7 @@ class UpdateTopBarMenuTransformerTest { private val coldWallet: UserWallet.Cold = mockk(relaxed = true) private val hotWallet: UserWallet.Hot = mockk(relaxed = true) private val cardTypesResolver: CardTypesResolver = mockk(relaxed = true) - private val onGenerateExtendedKey: () -> Unit = mockk(relaxed = true) - private val onHideClick: () -> Unit = mockk(relaxed = true) + private val clickIntents: TokenDetailsClickIntents = mockk(relaxed = true) @BeforeEach fun setUp() { @@ -77,8 +77,8 @@ class UpdateTopBarMenuTransformerTest { assertThat(result.topAppBarUM.menuItems).hasSize(1) result.topAppBarUM.menuItems.single().onClick() - verify(exactly = 1) { onHideClick.invoke() } - verify(exactly = 0) { onGenerateExtendedKey.invoke() } + verify(exactly = 1) { clickIntents.onHideClick() } + verify(exactly = 0) { clickIntents.onGenerateExtendedKey() } } @Test @@ -169,6 +169,48 @@ class UpdateTopBarMenuTransformerTest { assertThat(result.marketPriceBlockState).isSameInstanceAs(state.marketPriceBlockState) } + @Test + fun `GIVEN dynamic addresses available WHEN transform THEN dynamic addresses item is shown first`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = false, + isXPubSupported = false, + isDynamicAddressesAvailable = true, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN — Dynamic addresses item first, Hide token second + assertThat(result.topAppBarUM.menuItems).hasSize(2) + + result.topAppBarUM.menuItems.first().onClick() + verify(exactly = 1) { clickIntents.onDynamicAddressesClick() } + } + + @Test + fun `GIVEN dynamic addresses unavailable WHEN transform THEN dynamic addresses item is hidden`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = false, + isXPubSupported = false, + isDynamicAddressesAvailable = false, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN — only Hide token + assertThat(result.topAppBarUM.menuItems).hasSize(1) + + result.topAppBarUM.menuItems.single().onClick() + verify(exactly = 0) { clickIntents.onDynamicAddressesClick() } + } + @Test fun `GIVEN callbacks WHEN menu items invoked THEN callbacks are dispatched`() { // GIVEN @@ -177,6 +219,7 @@ class UpdateTopBarMenuTransformerTest { userWallet = coldWallet, hasDerivations = true, isXPubSupported = true, + isDynamicAddressesAvailable = true, ) // WHEN @@ -184,20 +227,22 @@ class UpdateTopBarMenuTransformerTest { result.topAppBarUM.menuItems.forEach { it.onClick() } // THEN - verify(exactly = 1) { onGenerateExtendedKey.invoke() } - verify(exactly = 1) { onHideClick.invoke() } + verify(exactly = 1) { clickIntents.onDynamicAddressesClick() } + verify(exactly = 1) { clickIntents.onGenerateExtendedKey() } + verify(exactly = 1) { clickIntents.onHideClick() } } private fun createTransformer( userWallet: UserWallet, hasDerivations: Boolean, isXPubSupported: Boolean, + isDynamicAddressesAvailable: Boolean = false, ) = UpdateTopBarMenuTransformer( userWallet = userWallet, hasDerivations = hasDerivations, - isXPubSupported = isXPubSupported, - onGenerateExtendedKey = onGenerateExtendedKey, - onHideClick = onHideClick, + isXpubSupported = isXPubSupported, + isDynamicAddressesAvailable = isDynamicAddressesAvailable, + clickIntents = clickIntents, ) private fun initialState(): TokenDetailsUM = TokenDetailsUM(