Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-12 18:18:30 +01:00
parent e5475e26e1
commit 7e42240fc6
8 changed files with 199 additions and 17 deletions

View file

@ -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
}
}

View file

@ -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<AccountStatus.Payment>()),
)
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<AccountStatus>): AccountStatusList {
return mockk {
every { this@mockk.accountStatuses } returns accountStatuses
}
}
private fun cryptoPortfolioWithCurrencies(count: Int): AccountStatus.CryptoPortfolio {
val tokenList = mockk<TokenList> {
every { flattenCurrencies() } returns List(count) { mockk<CryptoCurrencyStatus>() }
}
return mockk {
every { this@mockk.tokenList } returns tokenList
}
}
}

View file

@ -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,
)

View file

@ -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<AddAndManageBottomSheetComponent.Params>()
val portfolioSelectorNavigation: SlotNavigation<Unit> = SlotNavigation()
val portfolioFetcher: PortfolioFetcher by lazy {
portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.Wallet(params.userWalletId),
scope = modelScope,
)
}
val state: StateFlow<AddAndManageState>
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) }
}
}
}

View file

@ -0,0 +1,5 @@
package com.tangem.feature.wallet.child.managetokens.model
data class AddAndManageState(
val shouldShowOrganize: Boolean,
)

View file

@ -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 = {},
)
}

View file

@ -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<String, BigDecimal>,
private val stakingAvailabilityMap: Map<CryptoCurrency, StakingAvailability>,
private val shouldShowMainPromo: Boolean,
shouldShowMainPromo: Boolean,
private val isAddAndManageTokensEnabled: Boolean,
) : Converter<WalletTokensListState, WalletTokensListState> {
@ -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(),

View file

@ -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<UserWalletId>()) } 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<AccountStatus> = currencyCounts.map { count ->
val tokenList = mockk<TokenList> {
every { flattenCurrencies() } returns List(count) { mockk<CryptoCurrencyStatus>() }
}
mockk<AccountStatus.CryptoPortfolio> {
every { this@mockk.tokenList } returns tokenList
}
}
return mockk {
every { accountStatuses } returns statuses
}
}
}