Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-21 14:44:33 +07:00
parent 62fca32e89
commit b27e8171fb
19 changed files with 337 additions and 200 deletions

View file

@ -0,0 +1,117 @@
package com.tangem.common.ui.account
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.account.AccountName
@Composable
fun PortfolioSelectRow(state: PortfolioSelectUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.clickable(enabled = state.isMultiChoice, onClick = state.onClick)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
val leftText = if (state.isAccountMode) R.string.account_details_title else R.string.wc_common_wallet
Text(
modifier = Modifier.weight(1f),
text = stringResourceSafe(leftText),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
SpacerW12()
if (state.icon != null) {
AccountIcon(
name = state.name,
icon = state.icon,
size = AccountIconSize.Small,
)
}
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 4.dp),
text = state.name.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
if (state.isMultiChoice) {
Icon(
modifier = Modifier
.size(width = 18.dp, height = 24.dp),
painter = painterResource(id = R.drawable.ic_select_18_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
}
}
@Immutable
data class PortfolioSelectUM(
val icon: CryptoPortfolioIconUM?,
val name: TextReference,
val isAccountMode: Boolean,
val isMultiChoice: Boolean,
val onClick: () -> Unit,
)
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PortfolioSelectRowPreview(@PreviewParameter(PreviewProvider::class) state: PortfolioSelectUM) {
TangemThemePreview {
PortfolioSelectRow(
state = state,
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
)
}
}
private class PreviewProvider : PreviewParameterProvider<PortfolioSelectUM> {
val account
get() = PortfolioSelectUM(
icon = AccountIconPreviewData.randomAccountIcon(),
name = AccountName.DefaultMain.toUM().value,
isAccountMode = true,
isMultiChoice = true,
onClick = {},
)
val wallet
get() = PortfolioSelectUM(
icon = null,
name = stringReference("Wallet Name"),
isMultiChoice = false,
isAccountMode = false,
onClick = {},
)
override val values: Sequence<PortfolioSelectUM>
get() = sequenceOf(account, wallet)
}

View file

@ -22,7 +22,12 @@ interface PortfolioFetcher {
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
val balances: Map<UserWallet, PortfolioBalance>,
)
) {
val isSingleChoice: Boolean = balances.values
.map { it.accountsBalance.accountStatuses }
.flatten().size == 1
}
data class PortfolioBalance(
val userWallet: UserWallet,

View file

@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.Account
@ -20,12 +21,13 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon
import com.tangem.features.account.details.entity.AccountDetailsUM
import com.tangem.features.account.details.entity.AccountDetailsUM.ArchiveMode
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class AccountDetailsModel @Inject constructor(
paramsContainer: ParamsContainer,
@ -33,13 +35,24 @@ internal class AccountDetailsModel @Inject constructor(
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model() {
private val params = paramsContainer.require<AccountDetailsComponent.Params>()
val uiState: StateFlow<AccountDetailsUM> get() = _uiState
private val _uiState: MutableStateFlow<AccountDetailsUM> = MutableStateFlow(getInitialState())
private val _uiState: MutableStateFlow<AccountDetailsUM> = MutableStateFlow(buildUI(params.account))
private val accountId = params.account.accountId
init {
singleAccountStatusListSupplier(accountId.userWalletId)
.map { it.accountStatuses.find { status -> status.account.accountId == accountId }?.account }
.filterNotNull()
.distinctUntilChanged()
.map { account -> buildUI(account) }
.launchIn(modelScope)
}
private fun onEditAccountClick() {
router.push(AppRoute.EditAccount(params.account))
@ -78,7 +91,12 @@ internal class AccountDetailsModel @Inject constructor(
}
private fun archiveCryptoPortfolio() = modelScope.launch {
_uiState.update { it.toggleProgress(true) }
archiveCryptoPortfolioUseCase(params.account.accountId)
.onLeft { error ->
failedArchiveDialog(error)
_uiState.update { it.toggleProgress(false) }
}
.onRight {
val message = resourceReference(R.string.account_archive_success_message)
messageSender.send(ToastMessage(message = message))
@ -86,13 +104,37 @@ internal class AccountDetailsModel @Inject constructor(
}
}
private fun getInitialState(): AccountDetailsUM {
val account = params.account
private fun failedArchiveDialog(error: ArchiveCryptoPortfolioUseCase.Error) {
// todo account referral case
val titleRes = when (error) {
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet,
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound,
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated,
is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed,
-> R.string.common_something_went_wrong
}
val messageRes = when (error) {
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet,
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound,
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated,
is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed,
-> R.string.account_could_not_archive
}
val dialogMessage = DialogMessage(
title = resourceReference(titleRes),
message = resourceReference(messageRes),
)
messageSender.send(dialogMessage)
}
private fun buildUI(account: Account): AccountDetailsUM {
val archiveMode = when (account) {
is Account.CryptoPortfolio -> when (account.isMainAccount) {
true -> AccountDetailsUM.ArchiveMode.None
false -> AccountDetailsUM.ArchiveMode.Available(
true -> ArchiveMode.None
false -> ArchiveMode.Available(
onArchiveAccountClick = ::onArchiveAccountClick,
isLoading = false,
)
}
}
@ -108,4 +150,9 @@ internal class AccountDetailsModel @Inject constructor(
isManageTokensAvailable = isMultiCurrency,
)
}
private fun AccountDetailsUM.toggleProgress(isLoading: Boolean): AccountDetailsUM {
val archiveMode = this.archiveMode as? ArchiveMode.Available ?: return this
return this.copy(archiveMode = archiveMode.copy(isLoading = isLoading))
}
}

View file

@ -17,6 +17,7 @@ internal data class AccountDetailsUM(
data object None : ArchiveMode
data class Available(
val onArchiveAccountClick: () -> Unit,
val isLoading: Boolean,
) : ArchiveMode
}
}

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -91,21 +92,33 @@ private fun ArchiveAccountRow(state: AccountDetailsUM.ArchiveMode.Available) {
.fillMaxWidth()
.clip(RoundedCornerShape(TangemTheme.dimens.radius12))
.background(TangemTheme.colors.background.primary)
.clickable(onClick = state.onArchiveAccountClick)
.clickable(enabled = !state.isLoading, onClick = state.onArchiveAccountClick)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Icon(
tint = TangemTheme.colors.icon.warning,
imageVector = ImageVector.vectorResource(id = R.drawable.ic_archive_24),
contentDescription = null,
)
Text(
text = stringResourceSafe(R.string.account_details_archive),
color = TangemTheme.colors.text.warning,
style = TangemTheme.typography.subtitle1,
)
if (state.isLoading) {
CircularProgressIndicator(
color = TangemTheme.colors.text.disabled,
modifier = Modifier.size(TangemTheme.dimens.size24),
)
Text(
text = stringResourceSafe(R.string.account_details_archive),
color = TangemTheme.colors.text.disabled,
style = TangemTheme.typography.subtitle1,
)
} else {
Icon(
tint = TangemTheme.colors.icon.warning,
imageVector = ImageVector.vectorResource(id = R.drawable.ic_archive_24),
contentDescription = null,
)
Text(
text = stringResourceSafe(R.string.account_details_archive),
color = TangemTheme.colors.text.warning,
style = TangemTheme.typography.subtitle1,
)
}
}
}
@ -174,6 +187,12 @@ private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::
}
}
private val archiveModeAvailable
get() = AccountDetailsUM.ArchiveMode.Available(
onArchiveAccountClick = {},
isLoading = false,
)
private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountDetailsUM>(
buildList {
val accountName = "Main"
@ -182,16 +201,13 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountD
onCloseClick = {},
onAccountEditClick = {},
onManageTokensClick = {},
archiveMode = AccountDetailsUM.ArchiveMode.Available(
onArchiveAccountClick = {},
),
archiveMode = archiveModeAvailable,
accountName = stringReference(accountName),
accountIcon = portfolioIcon,
isManageTokensAvailable = true,
)
add(first)
portfolioIcon = AccountIconPreviewData.randomAccountIcon(letter = true)
add(first.copy(accountIcon = portfolioIcon))
add(first.copy(archiveMode = archiveModeAvailable.copy(isLoading = true)))
add(first.copy(archiveMode = AccountDetailsUM.ArchiveMode.None))
add(first.copy(isManageTokensAvailable = false))
},

View file

@ -10,6 +10,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -42,13 +43,17 @@ internal class PortfolioSelectorModel @Inject constructor(
field = MutableStateFlow<PortfolioSelectorUM>(emptyState())
init {
val selectedAccountState = selectorController.selectedAccount
.stateIn(modelScope, started = SharingStarted.Eagerly, initialValue = null)
combine(
flow = isAccountsModeEnabledUseCase(),
flow2 = balanceFetcher.data,
flow3 = walletImageFetcher.allWallets(ArtworkSize.SMALL),
flow4 = selectorController.isEnabled,
transform = { isAccountsMode, portfolioData, artworks, isEnabled ->
val uiList = buildUiList(isAccountsMode, portfolioData, artworks, isEnabled)
flow5 = selectedAccountState,
transform = { isAccountsMode, portfolioData, artworks, isEnabled, selectedAccount ->
val uiList = buildUiList(isAccountsMode, portfolioData, artworks, isEnabled, selectedAccount)
val title = when (isAccountsMode) {
true -> resourceReference(R.string.common_choose_account)
false -> resourceReference(R.string.common_choose_wallet)
@ -68,15 +73,17 @@ internal class PortfolioSelectorModel @Inject constructor(
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
isEnabled: (UserWallet, AccountStatus) -> Boolean,
selectedAccount: AccountId?,
): List<PortfolioSelectorItemUM> = when (isAccountsMode) {
true -> buildAccountsList(portfolioData, artworks, isEnabled)
false -> buildWalletList(portfolioData, artworks, isEnabled)
true -> buildAccountsList(portfolioData, artworks, isEnabled, selectedAccount)
false -> buildWalletList(portfolioData, artworks, isEnabled, selectedAccount)
}
private fun buildWalletList(
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
isEnabled: (UserWallet, AccountStatus) -> Boolean,
selectedAccount: AccountId?,
): List<PortfolioSelectorItemUM> = buildList {
val appCurrency = portfolioData.appCurrency
val isBalanceHidden = portfolioData.isBalanceHidden
@ -94,12 +101,14 @@ internal class PortfolioSelectorModel @Inject constructor(
isAuthMode = false,
).convert(wallet)
if (walletItemUM.isEnabled) {
val isEnabledByFeature = isEnabled(wallet, portfolio.accountsBalance.mainAccount)
val mainAccount = portfolio.accountsBalance.mainAccount
val isEnabledByFeature = isEnabled(wallet, mainAccount)
val finalWalletItemUM =
if (isEnabledByFeature) walletItemUM else walletItemUM.copy(isEnabled = false)
add(PortfolioSelectorItemUM.Portfolio(finalWalletItemUM))
val isSelected = mainAccount.isSelected(selectedAccount)
add(PortfolioSelectorItemUM.Portfolio(finalWalletItemUM, isSelected))
} else {
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM, isSelected = false))
}
}
if (lockedWallets.isNotEmpty()) {
@ -116,6 +125,7 @@ internal class PortfolioSelectorModel @Inject constructor(
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
isEnabled: (UserWallet, AccountStatus) -> Boolean,
selectedAccount: AccountId?,
): List<PortfolioSelectorItemUM> = buildList {
val appCurrency = portfolioData.appCurrency
val isBalanceHidden = portfolioData.isBalanceHidden
@ -133,7 +143,7 @@ internal class PortfolioSelectorModel @Inject constructor(
isAuthMode = false,
).convert(wallet)
if (!walletItemUM.isEnabled) {
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM, false))
return@forEach
}
@ -156,7 +166,8 @@ internal class PortfolioSelectorModel @Inject constructor(
isEnabled = isEnabledByFeature,
isBalanceHidden = isBalanceHidden,
).convert(account)
add(PortfolioSelectorItemUM.Portfolio(accountItemUM))
val isSelected = accountStatus.isSelected(selectedAccount)
add(PortfolioSelectorItemUM.Portfolio(accountItemUM, isSelected))
}
}
if (lockedWallets.isNotEmpty()) {
@ -169,6 +180,8 @@ internal class PortfolioSelectorModel @Inject constructor(
}
}
private fun AccountStatus.isSelected(selectedId: AccountId?) = this.account.accountId == selectedId
private fun emptyState() = PortfolioSelectorUM(
items = persistentListOf(),
title = TextReference.EMPTY,

View file

@ -21,6 +21,7 @@ sealed interface PortfolioSelectorItemUM {
data class Portfolio(
val item: UserWalletItemUM,
val isSelected: Boolean,
) : PortfolioSelectorItemUM {
override val id: String = item.id.stringValue
}

View file

@ -1,7 +1,9 @@
package com.tangem.features.account.selector.ui
import android.content.res.Configuration
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
@ -19,6 +21,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.userwallet.UserWalletItemRow
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
@ -68,14 +71,21 @@ internal fun PortfolioSelectorContent(
)
}
val portfolioShape = RoundedCornerShape(TangemTheme.dimens.radius14)
val border = BorderStroke(
width = 1.dp,
color = TangemTheme.colors.text.accent,
)
when (item) {
is PortfolioSelectorItemUM.Portfolio -> UserWalletItemRow(
state = item.item,
modifier = offsetModifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size68)
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.clip(portfolioShape)
.background(TangemTheme.colors.background.action)
.conditional(item.isSelected) { border(border, portfolioShape) }
.clickable(enabled = item.item.isEnabled, onClick = item.item.onClick)
.padding(all = TangemTheme.dimens.spacing12)
.conditional(!item.item.isEnabled) { alpha(DISABLED_WALLET_ALPHA) },
@ -158,16 +168,16 @@ internal object PortfolioSelectorPreviewData {
name = stringReference("Tangem 2.0"),
).let(::add)
accountItem
.let { PortfolioSelectorItemUM.Portfolio(it) }
.let { PortfolioSelectorItemUM.Portfolio(it, false) }
.let(::add)
lockedAccountItem
.let { PortfolioSelectorItemUM.Portfolio(it) }
.let { PortfolioSelectorItemUM.Portfolio(it, false) }
.let(::add)
PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = stringReference("Tangem White"),
).let(::add)
accountItem.let { PortfolioSelectorItemUM.Portfolio(it) }
accountItem.let { PortfolioSelectorItemUM.Portfolio(it, true) }
.let(::add)
}
@ -177,26 +187,26 @@ internal object PortfolioSelectorPreviewData {
id = UUID.randomUUID().toString(),
name = resourceReference(R.string.common_locked_wallets),
).let(::add)
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem, false))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem, false))
}
val walletList
get() = buildList {
add(PortfolioSelectorItemUM.Portfolio(walletItem))
add(PortfolioSelectorItemUM.Portfolio(walletItem))
add(PortfolioSelectorItemUM.Portfolio(walletItem, false))
add(PortfolioSelectorItemUM.Portfolio(walletItem, true))
}
val lockedWalletList
get() = buildList {
add(PortfolioSelectorItemUM.Portfolio(walletItem))
add(PortfolioSelectorItemUM.Portfolio(walletItem, true))
val title = PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = resourceReference(R.string.common_locked_wallets),
)
add(title)
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem, false))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem, false))
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.markets.portfolio.add.impl.model
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.common.ui.account.toUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.ParamsContainer
@ -33,7 +34,7 @@ internal class AddTokenUiBuilder @Inject constructor(
)
}
private fun createPortfolio(selectedPortfolio: SelectedPortfolio): AddTokenUM.Portfolio {
private fun createPortfolio(selectedPortfolio: SelectedPortfolio): PortfolioSelectUM {
val accountIcon: CryptoPortfolioIconUM?
val portfolioName: TextReference
when (selectedPortfolio.isAccountMode) {
@ -49,10 +50,11 @@ internal class AddTokenUiBuilder @Inject constructor(
}
}
}
return AddTokenUM.Portfolio(
accountIconUM = accountIcon,
return PortfolioSelectUM(
icon = accountIcon,
name = portfolioName,
editable = selectedPortfolio.availableMorePortfolio,
isAccountMode = selectedPortfolio.isAccountMode,
isMultiChoice = selectedPortfolio.availableMorePortfolio,
onClick = { params.callbacks.onChangePortfolioClick() },
)
}

View file

@ -22,14 +22,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountIcon
import com.tangem.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.account.PortfolioSelectRow
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.TokenItem
@ -63,12 +63,11 @@ internal fun AddTokenContent(state: AddTokenUM, modifier: Modifier = Modifier) {
SpacerH(TangemTheme.dimens.spacing14)
Column(
modifier = Modifier.background(
color = TangemTheme.colors.background.action,
shape = RoundedCornerShape(TangemTheme.dimens.radius14),
),
modifier = Modifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.background(color = TangemTheme.colors.background.action),
) {
PortfolioRow(state.portfolio)
PortfolioSelectRow(state.portfolio)
HorizontalDivider(
modifier = Modifier.padding(horizontal = 12.dp),
thickness = TangemTheme.dimens.size0_5,
@ -86,50 +85,6 @@ internal fun AddTokenContent(state: AddTokenUM, modifier: Modifier = Modifier) {
}
}
@Composable
private fun PortfolioRow(state: AddTokenUM.Portfolio, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.clickable(enabled = state.editable, onClick = state.onClick)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
val leftText = if (state.isAccountMode) R.string.account_details_title else R.string.wc_common_wallet
Text(
modifier = Modifier.weight(1f),
text = stringResourceSafe(leftText),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
SpacerW12()
if (state.accountIconUM != null) {
AccountIcon(
name = state.name,
icon = state.accountIconUM,
size = AccountIconSize.Small,
)
}
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 4.dp),
text = state.name.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
if (state.editable) {
Icon(
modifier = Modifier
.size(width = 18.dp, height = 24.dp)
.testTag(WalletConnectBottomSheetTestTags.NETWORKS_SELECTOR_ICON),
painter = painterResource(id = R.drawable.ic_select_18_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
}
}
@Composable
private fun NetworkRow(state: AddTokenUM.Network, modifier: Modifier = Modifier) {
Row(
@ -240,17 +195,19 @@ private class PreviewProvider : PreviewParameterProvider<AddTokenUM> {
)
val account
get() = AddTokenUM.Portfolio(
accountIconUM = AccountIconPreviewData.randomAccountIcon(),
get() = PortfolioSelectUM(
icon = AccountIconPreviewData.randomAccountIcon(),
name = AccountName.DefaultMain.toUM().value,
editable = true,
isAccountMode = true,
isMultiChoice = true,
onClick = {},
)
val wallet
get() = AddTokenUM.Portfolio(
accountIconUM = null,
name = stringReference("Wallet"),
editable = true,
get() = PortfolioSelectUM(
icon = null,
name = stringReference("Wallet Name"),
isMultiChoice = false,
isAccountMode = false,
onClick = {},
)
@ -271,7 +228,7 @@ private class PreviewProvider : PreviewParameterProvider<AddTokenUM> {
AddTokenUM(
tokenToAdd = tokenState,
network = networkUM.copy(editable = false),
portfolio = wallet.copy(editable = false),
portfolio = wallet.copy(isMultiChoice = false),
button = button.copy(
isEnabled = true,
isTangemIconVisible = true,

View file

@ -1,24 +1,16 @@
package com.tangem.features.markets.portfolio.add.impl.ui.state
import androidx.annotation.DrawableRes
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
data class AddTokenUM(
val tokenToAdd: TokenItemState,
val network: Network,
val portfolio: Portfolio,
val portfolio: PortfolioSelectUM,
val button: Button,
) {
data class Portfolio(
val accountIconUM: CryptoPortfolioIconUM?,
val name: TextReference,
val editable: Boolean,
val onClick: () -> Unit,
) {
val isAccountMode get() = accountIconUM != null
}
data class Network(
@DrawableRes val icon: Int,

View file

@ -1,5 +1,6 @@
package com.tangem.feature.referral.model
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedCryptoAmount
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedFiatAmount
@ -14,8 +15,10 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.feature.referral.models.ReferralStateHolder.AccountAward
import com.tangem.utils.converter.Converter
@Suppress("LongParameterList")
internal class AccountAwardConverter(
private val isBalanceHidden: Boolean,
private val isSingleAccount: Boolean,
private val appCurrency: AppCurrency,
private val awardCryptoCurrency: CryptoCurrency,
private val accountAwardToken: CryptoCurrencyStatus?,
@ -56,11 +59,15 @@ internal class AccountAwardConverter(
}
return AccountAward(
accountName = cryptoPortfolio.account.accountName.toUM().value,
accountIcon = cryptoPortfolio.account.icon.toUM(),
onAccountClick = onAccountClick,
isBalanceHidden = isBalanceHidden,
tokenState = tokenState,
accountSelectUM = PortfolioSelectUM(
icon = cryptoPortfolio.account.icon.toUM(),
name = cryptoPortfolio.account.accountName.toUM().value,
isAccountMode = true,
onClick = onAccountClick,
isMultiChoice = !isSingleAccount,
),
)
}
}

View file

@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
@ -23,7 +22,6 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.PortfolioId
@ -118,27 +116,27 @@ internal class ReferralModel @Inject constructor(
private fun combineAccountUI(referralData: ReferralData): Flow<AccountAward?> = combine(
flow = portfolioSelectorController.selectedAccountWithData(portfolioFetcher),
flow2 = getBalanceHidingSettingsFlow(),
flow3 = appCurrencyFlow(),
) { a, b, c -> Triple(a, b, c) }.mapLatest { triple ->
val (_, selectedAccount) = triple.first ?: return@mapLatest null
val isBalanceHidden = triple.second
val appCurrency: AppCurrency = triple.third
flow2 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
flow3 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
flow4 = portfolioFetcher.data,
) { pair, isBalanceHidden, appCurrency, portfolios ->
val selectedAccount = pair?.second ?: return@combine null
val awardCryptoCurrency = referralInteractor.getCryptoCurrency(
userWalletId = params.userWalletId,
tokenData = referralData.getToken(),
) ?: return@mapLatest null
) ?: return@combine null
val cryptoPortfolio = when (selectedAccount) {
is AccountStatus.CryptoPortfolio -> selectedAccount
}
val accountAwardToken = cryptoPortfolio.tokenList.flattenCurrencies()
.find { it.currency.id == awardCryptoCurrency.id }
return@mapLatest AccountAwardConverter(
return@combine AccountAwardConverter(
isBalanceHidden = isBalanceHidden,
awardCryptoCurrency = awardCryptoCurrency,
accountAwardToken = accountAwardToken,
cryptoPortfolio = cryptoPortfolio,
appCurrency = appCurrency,
isSingleAccount = portfolios.isSingleChoice,
onAccountClick = { bottomSheetNavigation.activate(Unit) },
).convert(Unit)
}
@ -277,18 +275,6 @@ internal class ReferralModel @Inject constructor(
}
}
private fun getBalanceHidingSettingsFlow(): Flow<Boolean> {
return getBalanceHidingSettingsUseCase()
.map { it.isBalanceHidden }
.distinctUntilChanged()
}
private fun appCurrencyFlow(): Flow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map { it.getOrElse { AppCurrency.Default } }
.distinctUntilChanged()
}
@Suppress("MagicNumber")
private fun ReferralInfo.getAddressValue(): String {
check(address.length > 5) { "Invalid address" }

View file

@ -1,9 +1,8 @@
package com.tangem.feature.referral.models
import androidx.annotation.DrawableRes
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.referral.domain.models.ExpectedAwards
internal data class ReferralStateHolder(
@ -64,8 +63,6 @@ internal data class ReferralStateHolder(
data class AccountAward(
val tokenState: TokenItemState,
val isBalanceHidden: Boolean,
val accountName: TextReference,
val accountIcon: CryptoPortfolioIconUM,
val onAccountClick: () -> Unit,
val accountSelectUM: PortfolioSelectUM,
)
}

View file

@ -15,6 +15,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
@ -26,14 +27,15 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountIcon
import com.tangem.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.account.PortfolioSelectRow
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.common.ui.account.toUM
import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
@ -41,13 +43,13 @@ import com.tangem.core.ui.components.snackbar.TangemSnackbar
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.ReferralProgramScreenTestTags
import com.tangem.domain.models.account.AccountName
import com.tangem.feature.referral.domain.models.ExpectedAward
import com.tangem.feature.referral.domain.models.ExpectedAwards
import com.tangem.feature.referral.models.DemoModeException
@ -390,10 +392,8 @@ private fun NonParticipateAccountAwardBlock(accountAward: AccountAward) {
Column(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.background(
color = TangemTheme.colors.background.primary,
shape = RoundedCornerShape(TangemTheme.dimens.spacing16),
),
.clip(RoundedCornerShape(TangemTheme.dimens.spacing16))
.background(color = TangemTheme.colors.background.primary),
) {
Text(
modifier = Modifier.padding(
@ -418,44 +418,7 @@ private fun NonParticipateAccountAwardBlock(accountAward: AccountAward) {
color = TangemTheme.colors.stroke.primary,
modifier = Modifier.padding(horizontal = 12.dp),
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
text = stringResourceSafe(R.string.account_details_title),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
AccountIcon(
name = accountAward.accountName,
icon = accountAward.accountIcon,
size = AccountIconSize.Small,
)
Text(
modifier = Modifier.padding(horizontal = 4.dp),
text = accountAward.accountName.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Icon(
modifier = Modifier.size(width = 18.dp, height = 24.dp),
painter = painterResource(id = R.drawable.ic_select_18_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
PortfolioSelectRow(accountAward.accountSelectUM)
}
}
@ -576,6 +539,13 @@ private fun Preview_ReferralScreen_NonParticipant() {
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ReferralScreen_NonParticipantAccount() {
val account = PortfolioSelectUM(
icon = AccountIconPreviewData.randomAccountIcon(),
name = AccountName.DefaultMain.toUM().value,
isAccountMode = true,
isMultiChoice = true,
onClick = {},
)
TangemThemePreview {
ReferralScreen(
stateHolder = ReferralStateHolder(
@ -588,9 +558,7 @@ private fun Preview_ReferralScreen_NonParticipantAccount() {
onParticipateClicked = {},
participateButtonIcon = R.drawable.ic_tangem_24,
accountAward = AccountAward(
onAccountClick = {},
accountIcon = AccountIconPreviewData.randomAccountIcon(),
accountName = stringReference("Main Account"),
accountSelectUM = account,
isBalanceHidden = false,
tokenState = TokenItemState.Content(
id = UUID.randomUUID().toString(),
@ -610,8 +578,8 @@ private fun Preview_ReferralScreen_NonParticipantAccount() {
),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "129,65 USDT"),
subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference("USDT")),
onItemClick = {},
onItemLongClick = {},
onItemClick = null,
onItemLongClick = null,
),
),
),

View file

@ -142,6 +142,7 @@ private fun WalletContent(
*/
val selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) }
val selectedWallet = state.wallets.getOrElse(selectedWalletIndex) { state.wallets[state.selectedWalletIndex] }
val selectedWalletChanged = rememberChangedOnce(selectedWalletIndex)
val listState = rememberLazyListState()
@ -239,6 +240,7 @@ private fun WalletContent(
contentItems(
state = selectedWallet,
selectedWalletChanged = selectedWalletChanged,
txHistoryItems = txHistoryItems,
isBalanceHidden = state.isHidingMode,
modifier = movableItemModifier,
@ -289,6 +291,14 @@ private fun WalletContent(
)
}
@Composable
private fun rememberChangedOnce(selectedWalletIndex: Int): Boolean {
var prev by remember { mutableIntStateOf(selectedWalletIndex) }
val changed = prev != selectedWalletIndex
SideEffect { prev = selectedWalletIndex }
return changed
}
@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable

View file

@ -22,10 +22,11 @@ internal fun LazyListScope.contentItems(
txHistoryItems: LazyPagingItems<TxHistoryState.TxHistoryItemState>?,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
selectedWalletChanged: Boolean,
) {
when (state) {
is WalletState.MultiCurrency -> {
tokensListItems(state.tokensListState, modifier, isBalanceHidden)
tokensListItems(state.tokensListState, modifier, isBalanceHidden, selectedWalletChanged)
}
is WalletState.SingleCurrency -> {
txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier)

View file

@ -27,6 +27,7 @@ internal fun LazyListScope.portfolioContentItems(
items: ImmutableList<TokensListItemUM.Portfolio>,
modifier: Modifier = Modifier,
isBalanceHidden: Boolean,
selectedWalletChanged: Boolean,
) {
items.forEachIndexed { index, item ->
portfolioTokensList(
@ -34,6 +35,7 @@ internal fun LazyListScope.portfolioContentItems(
modifier = modifier,
portfolioIndex = index,
isBalanceHidden = isBalanceHidden,
selectedWalletChanged = selectedWalletChanged,
)
}
}
@ -43,6 +45,7 @@ internal fun LazyListScope.portfolioTokensList(
modifier: Modifier,
portfolioIndex: Int,
isBalanceHidden: Boolean,
selectedWalletChanged: Boolean,
) {
val tokens = portfolio.tokens
val isExpanded = portfolio.isExpanded
@ -52,6 +55,7 @@ internal fun LazyListScope.portfolioTokensList(
modifier = modifier,
portfolioIndex = portfolioIndex,
isBalanceHidden = isBalanceHidden,
selectedWalletChanged = selectedWalletChanged,
)
if (!isExpanded) return
itemsIndexed(
@ -63,7 +67,7 @@ internal fun LazyListScope.portfolioTokensList(
val lastIndex = tokens.lastIndex.inc()
val isPreview = LocalInspectionMode.current
val appear = remember {
MutableTransitionState(isPreview).apply { targetState = true }
MutableTransitionState(selectedWalletChanged || isPreview).apply { targetState = true }
}
SlideInItemVisibility(
modifier = modifier
@ -92,6 +96,7 @@ private fun LazyListScope.portfolioItem(
modifier: Modifier,
portfolioIndex: Int,
isBalanceHidden: Boolean,
selectedWalletChanged: Boolean,
) {
val tokens = portfolio.tokens
val isExpanded = portfolio.isExpanded
@ -110,7 +115,7 @@ private fun LazyListScope.portfolioItem(
)
val isPreview = LocalInspectionMode.current
val appear = remember {
MutableTransitionState(isPreview).apply { targetState = true }
MutableTransitionState(selectedWalletChanged || isPreview).apply { targetState = true }
}
if (isExpanded) {
SlideInItemVisibility(

View file

@ -39,12 +39,14 @@ internal fun LazyListScope.tokensListItems(
state: WalletTokensListState,
modifier: Modifier = Modifier,
isBalanceHidden: Boolean,
selectedWalletChanged: Boolean,
) {
when (state) {
is WalletTokensListState.ContentState.PortfolioContent -> portfolioContentItems(
items = state.items,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
selectedWalletChanged = selectedWalletChanged,
)
is WalletTokensListState.ContentState.Content,
is WalletTokensListState.ContentState.Loading,