From 7e42240fc6eea7ae1900ff9634ac119211ed0815 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 May 2026 18:18:30 +0100 Subject: [PATCH] Updated on 2026-08-14 --- .../account/models/AccountStatusList.kt | 7 ++ .../models/AccountStatusListExtTest.kt | 96 +++++++++++++++++++ .../AddAndManageBottomSheetComponent.kt | 3 + .../managetokens/model/AddAndManageModel.kt | 18 +++- .../managetokens/model/AddAndManageState.kt | 5 + .../ui/AddAndManageBottomSheetContent.kt | 37 ++++--- .../converter/TokenListStateConverter.kt | 6 +- .../model/AddAndManageModelTest.kt | 44 +++++++++ 8 files changed, 199 insertions(+), 17 deletions(-) create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountStatusListExtTest.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/model/AddAndManageState.kt diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt index cc3b4c4204..c93443f0a6 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt @@ -57,4 +57,11 @@ data class AccountStatusList( groupType = groupType, ) } +} + +fun AccountStatusList.hasMultiCurrencyAccount(): Boolean = accountStatuses.any { status -> + when (status) { + is AccountStatus.CryptoPortfolio -> status.tokenList.flattenCurrencies().size > 1 + is AccountStatus.Payment -> false + } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountStatusListExtTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountStatusListExtTest.kt new file mode 100644 index 0000000000..d850b98442 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountStatusListExtTest.kt @@ -0,0 +1,96 @@ +package com.tangem.domain.account.models + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class AccountStatusListExtTest { + + @Test + fun `GIVEN no accounts WHEN hasMultiCurrencyAccount THEN returns false`() { + val accountList = createAccountStatusList(accountStatuses = emptyList()) + + val result = accountList.hasMultiCurrencyAccount() + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN only Payment accounts WHEN hasMultiCurrencyAccount THEN returns false`() { + val accountList = createAccountStatusList( + accountStatuses = listOf(mockk()), + ) + + val result = accountList.hasMultiCurrencyAccount() + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN CryptoPortfolio with single currency WHEN hasMultiCurrencyAccount THEN returns false`() { + val accountList = createAccountStatusList( + accountStatuses = listOf(cryptoPortfolioWithCurrencies(count = 1)), + ) + + val result = accountList.hasMultiCurrencyAccount() + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN CryptoPortfolio with no currencies WHEN hasMultiCurrencyAccount THEN returns false`() { + val accountList = createAccountStatusList( + accountStatuses = listOf(cryptoPortfolioWithCurrencies(count = 0)), + ) + + val result = accountList.hasMultiCurrencyAccount() + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN CryptoPortfolio with multiple currencies WHEN hasMultiCurrencyAccount THEN returns true`() { + val accountList = createAccountStatusList( + accountStatuses = listOf(cryptoPortfolioWithCurrencies(count = 2)), + ) + + val result = accountList.hasMultiCurrencyAccount() + + assertThat(result).isTrue() + } + + @Test + fun `GIVEN mix of single and multi currency portfolios WHEN hasMultiCurrencyAccount THEN returns true`() { + val accountList = createAccountStatusList( + accountStatuses = listOf( + cryptoPortfolioWithCurrencies(count = 1), + cryptoPortfolioWithCurrencies(count = 3), + ), + ) + + val result = accountList.hasMultiCurrencyAccount() + + assertThat(result).isTrue() + } + + private fun createAccountStatusList(accountStatuses: List): AccountStatusList { + return mockk { + every { this@mockk.accountStatuses } returns accountStatuses + } + } + + private fun cryptoPortfolioWithCurrencies(count: Int): AccountStatus.CryptoPortfolio { + val tokenList = mockk { + every { flattenCurrencies() } returns List(count) { mockk() } + } + return mockk { + every { this@mockk.tokenList } returns tokenList + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/AddAndManageBottomSheetComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/AddAndManageBottomSheetComponent.kt index 22715d7bb5..b327fcdd6a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/AddAndManageBottomSheetComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/AddAndManageBottomSheetComponent.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.child.managetokens import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot @@ -48,9 +49,11 @@ internal class AddAndManageBottomSheetComponent( @Composable override fun BottomSheet() { val portfolioSelectorSlot by portfolioSelectorSlot.subscribeAsState() + val state by model.state.collectAsStateWithLifecycle() AddAndManageBottomSheetContent( onAddTokensClick = model::onAddTokensClick, + shouldShowOrganizeButton = state.shouldShowOrganize, onOrganizeTokensClick = model::onOrganizeTokensClick, onDismiss = ::dismiss, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModel.kt index ce64ceba7a..84dd380d1c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModel.kt @@ -7,6 +7,8 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.account.models.hasMultiCurrencyAccount +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.models.account.AccountId import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent import com.tangem.feature.wallet.child.managetokens.analytics.PortfolioAnalyticsEvent @@ -14,7 +16,10 @@ import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorController import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -25,18 +30,20 @@ internal class AddAndManageModel @Inject constructor( private val portfolioFetcherFactory: PortfolioFetcher.Factory, private val analyticsEventHandler: AnalyticsEventHandler, val portfolioSelectorController: PortfolioSelectorController, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, ) : Model() { private val params = paramsContainer.require() val portfolioSelectorNavigation: SlotNavigation = SlotNavigation() - val portfolioFetcher: PortfolioFetcher by lazy { portfolioFetcherFactory.create( mode = PortfolioFetcher.Mode.Wallet(params.userWalletId), scope = modelScope, ) } + val state: StateFlow + field = MutableStateFlow(AddAndManageState(shouldShowOrganize = true)) val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback { override val onDismiss: () -> Unit = { portfolioSelectorNavigation.dismiss() } @@ -45,6 +52,7 @@ internal class AddAndManageModel @Inject constructor( init { observeAccountSelection() + updateShouldShowOrganizeButtonState() } fun onAddTokensClick() { @@ -85,4 +93,12 @@ internal class AddAndManageModel @Inject constructor( } } } + + private fun updateShouldShowOrganizeButtonState() { + modelScope.launch { + val accountStatusesList = singleAccountStatusListSupplier.getSyncOrNull(params.userWalletId) + val hasMultiCurrencyAccount = accountStatusesList?.hasMultiCurrencyAccount() == true + state.update { it.copy(shouldShowOrganize = hasMultiCurrencyAccount) } + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/model/AddAndManageState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/model/AddAndManageState.kt new file mode 100644 index 0000000000..e121302c16 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/model/AddAndManageState.kt @@ -0,0 +1,5 @@ +package com.tangem.feature.wallet.child.managetokens.model + +data class AddAndManageState( + val shouldShowOrganize: Boolean, +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContent.kt index 5534c0cb4f..e41627a9ec 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContent.kt @@ -31,6 +31,7 @@ import com.tangem.core.ui.res.TangemThemePreview @Composable internal fun AddAndManageBottomSheetContent( onAddTokensClick: () -> Unit, + shouldShowOrganizeButton: Boolean, onOrganizeTokensClick: () -> Unit, onDismiss: () -> Unit, ) { @@ -53,6 +54,7 @@ internal fun AddAndManageBottomSheetContent( content = { AddAndManageContent( onAddTokensClick = onAddTokensClick, + shouldShowOrganizeButton = shouldShowOrganizeButton, onOrganizeTokensClick = onOrganizeTokensClick, ) }, @@ -60,7 +62,11 @@ internal fun AddAndManageBottomSheetContent( } @Composable -private fun AddAndManageContent(onAddTokensClick: () -> Unit, onOrganizeTokensClick: () -> Unit) { +private fun AddAndManageContent( + onAddTokensClick: () -> Unit, + shouldShowOrganizeButton: Boolean, + onOrganizeTokensClick: () -> Unit, +) { Column( modifier = Modifier.padding( start = 16.dp, @@ -75,23 +81,25 @@ private fun AddAndManageContent(onAddTokensClick: () -> Unit, onOrganizeTokensCl onClick = onAddTokensClick, modifier = Modifier.roundedShapeItemDecoration( currentIndex = 0, - lastIndex = 1, - addDefaultPadding = false, - backgroundColor = TangemTheme.colors.background.action, - ), - ) - AddAndManageRow( - iconRes = R.drawable.ic_filter_default_24, - title = ResR.string.add_and_manage_sheet_organize_title, - subtitle = ResR.string.add_and_manage_sheet_organize_subtitle, - onClick = onOrganizeTokensClick, - modifier = Modifier.roundedShapeItemDecoration( - currentIndex = 1, - lastIndex = 1, + lastIndex = if (shouldShowOrganizeButton) 1 else 0, addDefaultPadding = false, backgroundColor = TangemTheme.colors.background.action, ), ) + if (shouldShowOrganizeButton) { + AddAndManageRow( + iconRes = R.drawable.ic_filter_default_24, + title = ResR.string.add_and_manage_sheet_organize_title, + subtitle = ResR.string.add_and_manage_sheet_organize_subtitle, + onClick = onOrganizeTokensClick, + modifier = Modifier.roundedShapeItemDecoration( + currentIndex = 1, + lastIndex = 1, + addDefaultPadding = false, + backgroundColor = TangemTheme.colors.background.action, + ), + ) + } } } @@ -152,6 +160,7 @@ private fun AddAndManageBottomSheetContent_Preview() { TangemThemePreview { AddAndManageContent( onAddTokensClick = {}, + shouldShowOrganizeButton = true, onOrganizeTokensClick = {}, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 8d23a44a9c..7eb8860cc4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.models.hasMultiCurrencyAccount import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.TotalFiatBalance @@ -40,7 +41,7 @@ internal class TokenListStateConverter( private val clickIntents: WalletClickIntents, private val yieldModuleApyMap: Map, private val stakingAvailabilityMap: Map, - private val shouldShowMainPromo: Boolean, + shouldShowMainPromo: Boolean, private val isAddAndManageTokensEnabled: Boolean, ) : Converter { @@ -169,7 +170,8 @@ internal class TokenListStateConverter( } private fun getOrganizeTokensButtonStateV2(accountList: AccountStatusList): WalletOrganizeTokensButtonConfig? { - return if (accountList.flattenCurrencies().size > 1 && !isSingleCurrencyWalletWithToken()) { + val shouldShowOrganizeIfOldButton = accountList.hasMultiCurrencyAccount() || isAddAndManageTokensEnabled + return if (shouldShowOrganizeIfOldButton && !isSingleCurrencyWalletWithToken()) { WalletOrganizeTokensButtonConfig( textRes = organizeButtonTextRes(), iconRes = organizeButtonIconRes(), diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModelTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModelTest.kt index 73e1539274..451f60cca2 100644 --- a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModelTest.kt +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModelTest.kt @@ -4,12 +4,18 @@ import com.google.common.truth.Truth.assertThat import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.decompose.model.MutableParamsContainer +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorController import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import io.mockk.slot @@ -38,6 +44,9 @@ internal class AddAndManageModelTest { private val portfolioSelectorController: PortfolioSelectorController = mockk(relaxed = true) { every { selectedAccount } returns flowOf(null) } + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk(relaxed = true) { + coEvery { getSyncOrNull(any()) } returns null + } private val onDismiss: () -> Unit = mockk(relaxed = true) private val onOrganizeTokensClick: () -> Unit = mockk(relaxed = true) @@ -58,6 +67,7 @@ internal class AddAndManageModelTest { portfolioFetcherFactory = portfolioFetcherFactory, analyticsEventHandler = analyticsEventHandler, portfolioSelectorController = portfolioSelectorController, + singleAccountStatusListSupplier = singleAccountStatusListSupplier, ) @Test @@ -98,4 +108,38 @@ internal class AddAndManageModelTest { verify(exactly = 1) { onDismiss() } verify(exactly = 1) { onOrganizeTokensClick() } } + + @Test + fun `GIVEN wallet has multi currency account WHEN model is created THEN shouldShowOrganize is true`() = runTest { + coEvery { singleAccountStatusListSupplier.getSyncOrNull(userWalletId) } returns + accountStatusListWithCurrencyCounts(2) + + val model = createModel() + + assertThat(model.state.value.shouldShowOrganize).isTrue() + } + + @Test + fun `GIVEN wallet has no multi currency account WHEN model is created THEN shouldShowOrganize is false`() = runTest { + coEvery { singleAccountStatusListSupplier.getSyncOrNull(userWalletId) } returns + accountStatusListWithCurrencyCounts(1) + + val model = createModel() + + assertThat(model.state.value.shouldShowOrganize).isFalse() + } + + private fun accountStatusListWithCurrencyCounts(vararg currencyCounts: Int): AccountStatusList { + val statuses: List = currencyCounts.map { count -> + val tokenList = mockk { + every { flattenCurrencies() } returns List(count) { mockk() } + } + mockk { + every { this@mockk.tokenList } returns tokenList + } + } + return mockk { + every { accountStatuses } returns statuses + } + } } \ No newline at end of file