Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-22 16:48:40 +07:00
parent 9abc118480
commit d3138c662e
18 changed files with 288 additions and 108 deletions

View file

@ -205,8 +205,8 @@ internal class AccountCreateEditModel @Inject constructor(
is AccountCreateEditComponent.Params.Edit -> {
val oldName = params.account.accountName.toUM()
val isNewName = this.account.name != oldName
val isNewIcon = this.account.portfolioIcon != params.account.portfolioIcon
val isNewName = this.account.name.trim() != oldName
val isNewIcon = this.account.portfolioIcon != params.account.portfolioIcon.toUM()
isValidName && (isNewName || isNewIcon)
}
}
@ -275,4 +275,9 @@ internal class AccountCreateEditModel @Inject constructor(
)
messageSender.send(dialogMessage)
}
}
private fun AccountNameUM.trim(): AccountNameUM = when (this) {
is AccountNameUM.Custom -> this.toDomain().getOrNull()?.toUM() ?: this
AccountNameUM.DefaultMain -> this
}

View file

@ -12,7 +12,8 @@ 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.producer.SingleAccountProducer
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.Account
@ -35,7 +36,7 @@ internal class AccountDetailsModel @Inject constructor(
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
singleAccountSupplier: SingleAccountSupplier,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model() {
@ -46,22 +47,19 @@ internal class AccountDetailsModel @Inject constructor(
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) }
singleAccountSupplier(SingleAccountProducer.Params(accountId))
.onEach { account -> _uiState.update { buildUI(account) } }
.launchIn(modelScope)
}
private fun onEditAccountClick() {
router.push(AppRoute.EditAccount(params.account))
private fun onEditAccountClick(account: Account) {
router.push(AppRoute.EditAccount(account))
}
private fun onManageTokensClick() {
private fun onManageTokensClick(account: Account) {
val route = AppRoute.ManageTokens(
source = AppRoute.ManageTokens.Source.SETTINGS,
portfolioId = PortfolioId(params.account.accountId),
portfolioId = PortfolioId(account.accountId),
)
router.push(route)
}
@ -92,7 +90,7 @@ internal class AccountDetailsModel @Inject constructor(
private fun archiveCryptoPortfolio() = modelScope.launch {
_uiState.update { it.toggleProgress(true) }
archiveCryptoPortfolioUseCase(params.account.accountId)
archiveCryptoPortfolioUseCase(accountId)
.onLeft { error ->
failedArchiveDialog(error)
_uiState.update { it.toggleProgress(false) }
@ -138,14 +136,14 @@ internal class AccountDetailsModel @Inject constructor(
)
}
}
val isMultiCurrency = getUserWalletUseCase(params.account.accountId.userWalletId)
val isMultiCurrency = getUserWalletUseCase(account.accountId.userWalletId)
.getOrNull()?.isMultiCurrency ?: false
return AccountDetailsUM(
accountName = params.account.accountName.toUM().value,
accountIcon = params.account.portfolioIcon.toUM(),
accountName = account.accountName.toUM().value,
accountIcon = account.portfolioIcon.toUM(),
onCloseClick = { router.pop() },
onAccountEditClick = ::onEditAccountClick,
onManageTokensClick = ::onManageTokensClick,
onAccountEditClick = { onEditAccountClick(account) },
onManageTokensClick = { onManageTokensClick(account) },
archiveMode = archiveMode,
isManageTokensAvailable = isMultiCurrency,
)

View file

@ -17,6 +17,7 @@ import com.tangem.features.account.PortfolioSelectorComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.serialization.builtins.serializer
class DefaultReferralComponent @AssistedInject constructor(
private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
@ -27,7 +28,7 @@ class DefaultReferralComponent @AssistedInject constructor(
private val model: ReferralModel = getOrCreateModel(params)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = null,
serializer = Unit.serializer(),
handleBackButton = false,
childFactory = { configuration, context -> bottomSheetChild(context) },
)

View file

@ -85,8 +85,8 @@ internal class ReferralModel @Inject constructor(
private set
val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback {
override val onDismiss: () -> Unit = { bottomSheetNavigation::dismiss }
override val onBack: () -> Unit = { bottomSheetNavigation::dismiss }
override val onDismiss: () -> Unit = { bottomSheetNavigation.dismiss() }
override val onBack: () -> Unit = { bottomSheetNavigation.dismiss() }
}
init {
@ -115,7 +115,8 @@ internal class ReferralModel @Inject constructor(
}
private fun combineAccountUI(referralData: ReferralData): Flow<AccountAward?> = combine(
flow = portfolioSelectorController.selectedAccountWithData(portfolioFetcher),
flow = portfolioSelectorController.selectedAccountWithData(portfolioFetcher)
.onEach { bottomSheetNavigation.dismiss() },
flow2 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
flow3 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
flow4 = portfolioFetcher.data,

View file

@ -10,25 +10,33 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
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.AccountIcon
import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.components.PrimaryButtonIconStart
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.rows.RoundableCornersRow
import com.tangem.core.ui.extensions.pluralStringResourceSafe
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.referral.domain.models.ExpectedAward
import com.tangem.feature.referral.domain.models.ExpectedAwards
import com.tangem.feature.referral.models.ReferralStateHolder
import com.tangem.feature.referral.presentation.R
import kotlinx.coroutines.launch
@ -43,6 +51,7 @@ internal fun ParticipateBottomBlock(
onAgreementClick: () -> Unit,
onCopyClick: () -> Unit,
onShareClick: (String) -> Unit,
accountAward: ReferralStateHolder.AccountAward?,
) {
Column(
modifier = Modifier
@ -53,7 +62,7 @@ internal fun ParticipateBottomBlock(
.padding(horizontal = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
PersonalCodeCard(code = code)
PersonalCodeCard(code = code, accountAward = accountAward)
AdditionalButtons(
code = code,
shareLink = shareLink,
@ -242,19 +251,13 @@ private fun ExtraItems(extraItems: List<ExpectedAward>, overallItemsLastIndex: I
}
@Composable
private fun PersonalCodeCard(code: String) {
private fun PersonalCodeCard(code: String, accountAward: ReferralStateHolder.AccountAward?) {
Column(
modifier = Modifier
.shadow(
elevation = TangemTheme.dimens.elevation2,
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
)
.background(
color = TangemTheme.colors.background.secondary,
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
)
.clip(RoundedCornerShape(TangemTheme.dimens.radius12))
.background(color = TangemTheme.colors.background.primary)
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens.spacing12),
.padding(top = TangemTheme.dimens.spacing12),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
@ -270,6 +273,53 @@ private fun PersonalCodeCard(code: String) {
maxLines = 1,
style = TangemTheme.typography.h2,
)
if (accountAward != null) {
AwardAccount(accountAward)
} else {
SpacerH12()
}
}
}
@Composable
private fun AwardAccount(accountAward: ReferralStateHolder.AccountAward) {
Column {
HorizontalDivider(
thickness = 0.5.dp,
color = TangemTheme.colors.stroke.primary,
modifier = Modifier.padding(horizontal = 12.dp),
)
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
text = stringResourceSafe(R.string.account_for_rewards),
style = TangemTheme.typography.body1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = TangemTheme.colors.text.secondary,
)
SpacerW12()
val icon = accountAward.accountSelectUM.icon
if (icon != null) {
AccountIcon(
name = accountAward.accountSelectUM.name,
icon = icon,
size = AccountIconSize.Small,
)
}
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 4.dp),
text = accountAward.accountSelectUM.name.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
}
}
}
@ -351,10 +401,11 @@ private fun ParticipateBottomBlockPreview(
code = data.code,
shareLink = data.shareLink,
expectedAwards = data.expectedAwards,
onAgreementClick = data.onAgreementClick,
snackbarHostState = SnackbarHostState(),
onAgreementClick = data.onAgreementClick,
onCopyClick = data.onCopyClick,
onShareClick = data.onShareClick,
accountAward = data.accountAward,
)
}
}
@ -410,12 +461,18 @@ private class ParticipateBottomBlockDataProvider : CollectionPreviewParameterPro
purchasedWalletCount = 0,
expectedAwards = null,
),
ParticipateBottomBlockData(
purchasedWalletCount = 0,
expectedAwards = null,
accountAward = accountReward,
),
),
)
private data class ParticipateBottomBlockData(
val purchasedWalletCount: Int,
val expectedAwards: ExpectedAwards?,
val accountAward: ReferralStateHolder.AccountAward? = null,
val code: String = "x4JDK",
val shareLink: String = "",
val onAgreementClick: () -> Unit = {},

View file

@ -185,6 +185,7 @@ private fun ReferralInfo(
code = state.code,
shareLink = state.shareLink,
expectedAwards = state.expectedAwards,
accountAward = state.accountAward,
snackbarHostState = snackbarHostState,
onAgreementClick = onAgreementClick,
onCopyClick = stateHolder.analytics.onCopyClicked,
@ -479,6 +480,7 @@ private fun Preview_ReferralScreen_Participant_With_Referrals() {
code = "x4JdK",
shareLink = "",
url = "",
accountAward = accountReward,
expectedAwards = ExpectedAwards(
numberOfWallets = 5,
expectedAwards = listOf(
@ -535,17 +537,45 @@ private fun Preview_ReferralScreen_NonParticipant() {
}
}
internal val account = PortfolioSelectUM(
icon = AccountIconPreviewData.randomAccountIcon(),
name = AccountName.DefaultMain.toUM().value,
isAccountMode = true,
isMultiChoice = true,
onClick = {},
)
internal val accountReward
get() = AccountAward(
accountSelectUM = account,
isBalanceHidden = false,
tokenState = TokenItemState.Content(
id = UUID.randomUUID().toString(),
iconState = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_tron_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
shouldShowCustomBadge = false,
),
titleState = TokenItemState.TitleState.Content(
text = stringReference(value = "Tether"),
),
fiatAmountState = FiatAmountState.Content(
text = "129,65 $",
),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "129,65 USDT"),
subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference("USDT")),
onItemClick = null,
onItemLongClick = null,
),
)
@Preview(widthDp = 360, showBackground = true)
@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(
@ -557,31 +587,7 @@ private fun Preview_ReferralScreen_NonParticipantAccount() {
url = "",
onParticipateClicked = {},
participateButtonIcon = R.drawable.ic_tangem_24,
accountAward = AccountAward(
accountSelectUM = account,
isBalanceHidden = false,
tokenState = TokenItemState.Content(
id = UUID.randomUUID().toString(),
iconState = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_tron_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
shouldShowCustomBadge = false,
),
titleState = TokenItemState.TitleState.Content(
text = stringReference(value = "Tether"),
),
fiatAmountState = FiatAmountState.Content(
text = "129,65 $",
),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "129,65 USDT"),
subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference("USDT")),
onItemClick = null,
onItemLongClick = null,
),
),
accountAward = accountReward,
),
errorSnackbar = null,
analytics = Analytics(

View file

@ -106,6 +106,28 @@ internal object WalletScreenPreviewData {
),
)
private val emptyPortfolioContentState = WalletTokensListState.ContentState.PortfolioContent(
items = persistentListOf(
TokensListItemUM.Portfolio(
tokens = textContentTokensState.items.filterIsInstance<PortfolioTokensListItemUM>().toPersistentList(),
isExpanded = false,
isCollapsable = true,
tokenItemUM = AccountItemPreviewData.accountItem
.copy(iconState = AccountItemPreviewData.accountLetterIcon),
),
TokensListItemUM.Portfolio(
tokens = persistentListOf(),
isExpanded = true,
isCollapsable = true,
tokenItemUM = AccountItemPreviewData.accountItem,
),
),
organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig(
isEnabled = true,
onClick = {},
),
)
private val noteLockedCard by lazy {
WalletCardState.LockedContent(
id = UserWalletId(stringValue = "1"),
@ -208,4 +230,12 @@ internal object WalletScreenPreviewData {
multiWalletState.copy(tokensListState = portfolioContentState),
),
)
internal val accountScreenWithEmptyTokensState =
walletScreenState.copy(
wallets = persistentListOf(
singleWalletLockedState,
multiWalletState.copy(tokensListState = emptyPortfolioContentState),
),
)
}

View file

@ -100,6 +100,7 @@ internal class TokenListStateConverter(
appCurrency = appCurrency,
account = account,
onItemClick = onItemClick,
priceChangeLce = this.priceChangeLce,
)
val accountItem = converter.convert(tokenList.totalFiatBalance)
val tokenConverter = tokenStatusConverter(account.accountId)

View file

@ -69,6 +69,7 @@ import com.tangem.core.ui.utils.moveTo
import com.tangem.core.ui.utils.toPx
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenState
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenWithEmptyTokensState
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder
@ -753,6 +754,7 @@ private class WalletScreenPreviewProvider : PreviewParameterProvider<WalletScree
walletScreenState,
walletScreenState.copy(selectedWalletIndex = 1),
accountScreenState.copy(selectedWalletIndex = 1),
accountScreenWithEmptyTokensState.copy(selectedWalletIndex = 1),
)
}
// endregion

View file

@ -58,6 +58,25 @@ internal fun LazyListScope.portfolioTokensList(
selectedWalletChanged = selectedWalletChanged,
)
if (!isExpanded) return
if (tokens.isEmpty()) {
item(
key = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}",
contentType = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}",
) {
NonContentItemContent(
modifier = Modifier
.animateItem()
.roundedShapeItemDecoration(
radius = TangemTheme.dimens.radius14,
currentIndex = 1,
lastIndex = 1,
backgroundColor = TangemTheme.colors.background.primary,
)
.padding(vertical = TangemTheme.dimens.spacing28),
)
}
return
}
itemsIndexed(
items = tokens,
key = { _, item -> item.id },
@ -74,6 +93,7 @@ internal fun LazyListScope.portfolioTokensList(
.testModifier(indexWithHeader)
.animateItem()
.roundedShapeItemDecoration(
radius = TangemTheme.dimens.radius14,
currentIndex = indexWithHeader,
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors.background.primary,
@ -101,6 +121,12 @@ private fun LazyListScope.portfolioItem(
val tokens = portfolio.tokens
val isExpanded = portfolio.isExpanded
val lastIndex = when {
isExpanded && tokens.isEmpty() -> 1
isExpanded -> tokens.lastIndex.inc()
else -> 0
}
item(
key = "account-${portfolio.id}-isExpanded$isExpanded",
contentType = "account-isExpanded$isExpanded",
@ -110,27 +136,24 @@ private fun LazyListScope.portfolioItem(
.animateItem()
.roundedShapeItemDecoration(
currentIndex = 0,
lastIndex = if (isExpanded) tokens.lastIndex.inc() else 0,
radius = TangemTheme.dimens.radius14,
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors.background.primary,
)
val isPreview = LocalInspectionMode.current
val appear = remember {
MutableTransitionState(selectedWalletChanged || isPreview).apply { targetState = true }
MutableTransitionState(selectedWalletChanged || isPreview || tokens.isEmpty())
.apply { targetState = true }
}
if (isExpanded) {
SlideInItemVisibility(
modifier = anchorModifier,
visibleState = appear,
) {
val modifier = if (portfolio.tokens.isEmpty()) {
Modifier.padding(vertical = 8.dp)
} else {
Modifier.padding(top = 8.dp)
}
PortfolioListItem(
state = portfolio,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
modifier = Modifier.padding(vertical = 8.dp),
)
}
} else {

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
@ -25,7 +26,7 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
import kotlinx.collections.immutable.ImmutableList
private const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST"
internal const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST"
/**
* LazyList extension for [WalletTokensListState]
@ -91,27 +92,34 @@ private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) {
key = NON_CONTENT_TOKENS_LIST_KEY,
contentType = NON_CONTENT_TOKENS_LIST_KEY,
) {
Column(
NonContentItemContent(
modifier = modifier
.animateItem(fadeInSpec = null, fadeOutSpec = null)
.padding(top = TangemTheme.dimens.spacing96),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
painter = painterResource(id = R.drawable.ic_empty_64),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens.size64),
tint = TangemTheme.colors.icon.inactive,
)
)
}
}
Text(
text = stringResourceSafe(id = R.string.main_empty_tokens_list_message),
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing48),
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2,
)
}
@Composable
internal fun NonContentItemContent(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
painter = painterResource(id = R.drawable.ic_empty_64),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens.size64),
tint = TangemTheme.colors.icon.inactive,
)
Text(
text = stringResourceSafe(id = R.string.main_empty_tokens_list_message),
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing48),
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2,
)
}
}