Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-15 12:38:50 +05:00
parent 006669ae0d
commit b92bb00c7f
10 changed files with 110 additions and 18 deletions

View file

@ -12,6 +12,7 @@ import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.utils.extensions.isZero
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
import java.math.RoundingMode
/**
* Utility object for calculating the [PriceChange] of a cryptocurrency portfolio.
@ -50,11 +51,11 @@ object PriceChangeCalculator {
}
val total = statuses.sumOf {
val weight = it.value.fiatAmount.orZero().divide(balance)
val weight = it.value.fiatAmount.orZero().divide(balance, 2, RoundingMode.HALF_UP)
val priceChange = it.value.priceChange.orZero()
weight * priceChange
}
}.stripTrailingZeros()
return PriceChange(value = total, source = walletTotalFiatBalance.source).lceContent()
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.send.v2.api.subcomponents.destination.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.AccountTitleUM
import kotlinx.collections.immutable.ImmutableList
@Immutable
@ -15,6 +16,7 @@ sealed class DestinationUM {
val recent: ImmutableList<DestinationRecipientListUM>,
val wallets: ImmutableList<DestinationRecipientListUM>,
val networkName: String,
val accountTitleUM: AccountTitleUM?,
val isValidating: Boolean = false,
val isInitialized: Boolean = false,
val isRecentHidden: Boolean,

View file

@ -140,8 +140,8 @@ internal class SendAmountModel @Inject constructor(
flow2 = params.accountFlow,
flow3 = params.isAccountModeFlow,
) { newCryptoCurrencyStatus, account, isAccountsMode ->
maxAmountBoundary = MaxEnterAmountConverter().convert(newCryptoCurrencyStatus)
cryptoCurrencyStatus = newCryptoCurrencyStatus
maxAmountBoundary = MaxEnterAmountConverter().convert(cryptoCurrencyStatus)
initMinBoundary(cryptoCurrencyStatus, account, isAccountsMode)
}.flowOn(dispatchers.default)
.launchIn(modelScope)

View file

@ -11,6 +11,9 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.network.CryptoCurrencyAddress
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isLocked
@ -64,7 +67,10 @@ internal class SendDestinationModel @Inject constructor(
private val isSelfSendAvailableUseCase: IsSelfSendAvailableUseCase,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val parseQrCodeUseCase: ParseQrCodeUseCase,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val accountsFeatureToggles: AccountsFeatureToggles,
) : Model(), SendDestinationClickIntents {
private val params: SendDestinationComponentParams = paramsContainer.require()
@ -142,7 +148,7 @@ internal class SendDestinationModel @Inject constructor(
)
}
fun saveResult() {
private fun saveResult() {
val params = params as? SendDestinationComponentParams.DestinationParams ?: return
params.callback.onDestinationResult(uiState.value)
}
@ -193,12 +199,17 @@ internal class SendDestinationModel @Inject constructor(
).getOrElse { flowOf(emptyList()) }.map {
waitForDelay(RECENT_LOAD_DELAY) { it }
}.conflate(),
) { destinationWalletList, txHistoryList ->
flow3 = isAccountsModeEnabledUseCase().distinctUntilChanged(),
flow4 = if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCurrencyStatusUseCase(userWalletId, cryptoCurrency).distinctUntilChanged()
} else {
flowOf(null)
},
) { destinationWalletList, txHistoryList, isAccountsMode, accountCurrencyStatus ->
val isSelfSendAvailable = isSelfSendAvailableUseCase.invokeSync(
userWalletId = userWalletId,
network = cryptoCurrency.network,
)
_uiState.update(
SendDestinationRecentListTransformer(
cryptoCurrency = cryptoCurrency,
@ -206,9 +217,11 @@ internal class SendDestinationModel @Inject constructor(
isSelfSendAvailable = isSelfSendAvailable,
destinationWalletList = destinationWalletList,
txHistoryList = txHistoryList,
account = accountCurrencyStatus?.account,
isAccountsMode = isAccountsMode,
),
)
}.launchIn(modelScope)
}.flowOn(dispatchers.default).launchIn(modelScope)
}
private suspend fun List<UserWallet>.toAvailableWallets(): List<DestinationWalletUM> {

View file

@ -54,6 +54,7 @@ internal class SendDestinationInitialStateTransformer(
isValuePasted = false,
)
},
accountTitleUM = null,
wallets = loadingListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT),
recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT),
networkName = cryptoCurrency.network.name,

View file

@ -1,24 +1,42 @@
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.TxInfo
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientHistoryListConverter
import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientWalletListConverter
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM
import com.tangem.utils.StringsSigns
import com.tangem.utils.transformer.Transformer
@Suppress("LongParameterList")
internal class SendDestinationRecentListTransformer(
private val senderAddress: String?,
private val cryptoCurrency: CryptoCurrency,
private val isSelfSendAvailable: Boolean,
private val destinationWalletList: List<DestinationWalletUM>,
private val txHistoryList: List<TxInfo>,
private val account: Account.CryptoPortfolio?,
private val isAccountsMode: Boolean,
) : Transformer<DestinationUM> {
override fun transform(prevState: DestinationUM): DestinationUM {
val state = prevState as? DestinationUM.Content ?: return prevState
return state.copy(
accountTitleUM = if (account != null && isAccountsMode) {
AccountTitleUM.Account(
name = account.accountName.toUM().value,
icon = account.icon.toUM(),
prefixText = stringReference(StringsSigns.DOT),
)
} else {
AccountTitleUM.Text(TextReference.EMPTY)
},
wallets = SendRecipientWalletListConverter(
senderAddress = senderAddress,
isSelfSendAvailable = isSelfSendAvailable,

View file

@ -144,6 +144,7 @@ private class DestinationBlockPreviewProvider : PreviewParameterProvider<Destina
isValidating = false,
isInitialized = true,
isRecentHidden = false,
accountTitleUM = null,
)
override val values: Sequence<DestinationUM.Content>

View file

@ -23,15 +23,22 @@ 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.AccountNameUM
import com.tangem.common.ui.account.AccountTitle
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.features.send.v2.impl.R
import com.tangem.utils.StringsSigns
/**
* Row item with title and subtitle
@ -50,6 +57,7 @@ fun ListItemWithIcon(
subtitle: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
accountTitleUM: AccountTitleUM? = null,
info: String? = null,
subtitleEndOffset: Int = 0,
@DrawableRes subtitleIconRes: Int? = null,
@ -68,6 +76,7 @@ fun ListItemWithIcon(
subtitle = subtitle,
onClick = onClick,
info = info,
accountTitleUM = accountTitleUM,
subtitleEndOffset = subtitleEndOffset,
subtitleIconRes = subtitleIconRes,
modifier = modifier,
@ -80,6 +89,7 @@ fun ListItemWithIcon(
private fun ListItemWithIcon(
title: String,
subtitle: String,
accountTitleUM: AccountTitleUM?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
info: String? = null,
@ -115,7 +125,7 @@ private fun ListItemWithIcon(
ellipsis = TextEllipsis.Middle,
modifier = Modifier,
)
Row {
Row(verticalAlignment = Alignment.CenterVertically) {
if (subtitleIconRes != null) {
Icon(
painter = painterResource(id = subtitleIconRes),
@ -142,6 +152,13 @@ private fun ListItemWithIcon(
color = TangemTheme.colors.text.tertiary,
ellipsis = TextEllipsis.OffsetEnd(offsetEnd = offset),
)
if (accountTitleUM != null) {
AccountTitle(
accountTitleUM = accountTitleUM,
textStyle = TangemTheme.typography.caption2,
modifier = Modifier.padding(start = 4.dp),
)
}
}
}
}
@ -195,6 +212,7 @@ private fun ListItemWithIconPreview(
ListItemWithIcon(
title = config.title,
subtitle = config.subtitle,
accountTitleUM = config.accountTitleUM,
subtitleEndOffset = config.subtitleEndOffset,
subtitleIconRes = config.iconRes,
onClick = {},
@ -206,6 +224,7 @@ private fun ListItemWithIconPreview(
private data class ListItemWithIconPreviewConfig(
val title: String,
val subtitle: String,
val accountTitleUM: AccountTitleUM.Account? = null,
val info: String? = null,
val subtitleEndOffset: Int = 0,
val iconRes: Int? = null,
@ -240,6 +259,15 @@ private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvid
iconRes = R.drawable.ic_arrow_down_24,
isLoading = true,
),
ListItemWithIconPreviewConfig(
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
subtitle = "Wallet",
accountTitleUM = AccountTitleUM.Account(
name = AccountNameUM.DefaultMain.value,
icon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM(),
prefixText = stringReference(StringsSigns.DOT),
),
),
),
)
//endregion

View file

@ -21,7 +21,9 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.inputrow.InputRowRecipient
import com.tangem.core.ui.extensions.*
@ -46,7 +48,6 @@ internal fun SendDestinationContent(
if (state !is DestinationUM.Content) return
val recipients = state.recent
val wallets = state.wallets
val memoField = state.memoTextField
val address = state.addressTextField
val isValidating by remember(state.isValidating) { derivedStateOf { state.isValidating } }
val isError by remember(address.isError) { derivedStateOf { address.isError } }
@ -64,18 +65,23 @@ internal fun SendDestinationContent(
onQrCodeClick = clickIntents::onQrCodeScanClick,
)
memoField(
memoField = memoField,
memoField = state.memoTextField,
onMemoChange = clickIntents::onRecipientMemoValueChange,
)
listHeaderItem(
titleRes = R.string.send_recipient_wallets_title,
titleRes = when (state.accountTitleUM) {
is AccountTitleUM.Account -> R.string.common_accounts
else -> R.string.send_recipient_wallets_title
},
isLoading = state.accountTitleUM == null,
isVisible = wallets.isNotEmpty() && wallets.first().isVisible && !state.isRecentHidden,
isFirst = true,
)
listItem(
list = wallets,
isLast = recipients.any { !it.isVisible },
isBalanceHidden = isBalanceHidden,
accountTitleUM = state.accountTitleUM,
isBalanceHidden = false,
isRecentHidden = state.isRecentHidden,
onClick = { title ->
clickIntents.onRecipientAddressValueChange(
@ -86,6 +92,7 @@ internal fun SendDestinationContent(
)
listHeaderItem(
titleRes = R.string.send_recent_transactions,
isLoading = state.accountTitleUM == null,
isVisible = recipients.isNotEmpty() && recipients.first().isVisible && !state.isRecentHidden,
isFirst = wallets.any { !it.isVisible },
)
@ -180,7 +187,12 @@ private fun LazyListScope.memoField(
}
}
private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Boolean, isFirst: Boolean) {
private fun LazyListScope.listHeaderItem(
@StringRes titleRes: Int,
isVisible: Boolean,
isLoading: Boolean,
isFirst: Boolean,
) {
item(key = titleRes) {
AnimateRecentAppearance(isVisible) {
val (topPadding, paddingFromTop) = if (isFirst) {
@ -189,10 +201,8 @@ private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Bo
0.dp to 8.dp
}
val topRadius = if (isFirst) 16.dp else 0.dp
Text(
text = stringResourceSafe(titleRes),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
AnimatedContent(
targetState = isLoading,
modifier = Modifier
.fillMaxWidth()
.padding(top = topPadding)
@ -209,7 +219,22 @@ private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Bo
start = 12.dp,
end = 12.dp,
),
)
) { currentIsLoading ->
if (currentIsLoading) {
Box {
TextShimmer(
style = TangemTheme.typography.subtitle2,
text = stringResourceSafe(titleRes),
)
}
} else {
Text(
text = stringResourceSafe(titleRes),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
}
}
@ -220,6 +245,7 @@ private fun LazyListScope.listItem(
isBalanceHidden: Boolean,
isRecentHidden: Boolean,
onClick: (String) -> Unit,
accountTitleUM: AccountTitleUM? = null,
) {
items(
count = list.size,
@ -233,6 +259,7 @@ private fun LazyListScope.listItem(
ListItemWithIcon(
title = title,
subtitle = item.subtitle.orMaskWithStars(isBalanceHidden).resolveReference(),
accountTitleUM = accountTitleUM,
info = item.timestamp?.resolveReference(),
subtitleEndOffset = item.subtitleEndOffset,
subtitleIconRes = item.subtitleIconRes,

View file

@ -310,6 +310,7 @@ private fun SendWithSwapSuccessContent_Preview() {
isValidating = false,
isInitialized = false,
isRecentHidden = false,
accountTitleUM = AccountTitleUM.Text(resourceReference(R.string.send_recipient_wallets_title)),
),
feeSelectorUM = FeeSelectorUM.Content(
fees = TransactionFee.Single(