From 5fce0646d842f2568f77c15ec6df4d2d5ce393da Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Jun 2026 12:37:29 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../transactions/TransactionItem.kt | 36 +++++++++++ .../transactions/state/TransactionItemUM.kt | 9 ++- ...wapModelGetSelectApprovalTypeParamsTest.kt | 3 +- features/txhistory/impl/build.gradle.kts | 6 ++ ...HistoryItemToTransactionItemUMConverter.kt | 61 +++++++++++++------ ...oryItemToTransactionItemUMConverterTest.kt | 8 +-- 6 files changed, 99 insertions(+), 24 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionItem.kt index 59858ddf0e..a0c9c33daa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionItem.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -25,6 +26,8 @@ import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -201,9 +204,15 @@ private fun SubtitleText(subtitle: ContentSubtitle, status: Status, modifier: Mo overflow = TextOverflow.Ellipsis, modifier = modifier, ) + is ContentSubtitle.PlainAddress -> PlainAddressText( + subtitle = subtitle, + status = status, + modifier = modifier, + ) is ContentSubtitle.ExternalAddress -> InlineImageSubtitle( template = stringResourceSafe(subtitle.direction.templateResId(), subtitle.briefAddress), color = tertiary, + afterIconColor = if (isFailed) tertiary else primary, modifier = modifier, ) { IdentIcon( @@ -256,6 +265,33 @@ private fun SubtitleText(subtitle: ContentSubtitle, status: Status, modifier: Mo } } +@Composable +private fun PlainAddressText(subtitle: ContentSubtitle.PlainAddress, status: Status, modifier: Modifier = Modifier) { + val full = subtitle.text.resolveReference() + val highlightColor = if (status is Status.Failed) { + TangemTheme.colors2.text.neutral.tertiary + } else { + TangemTheme.colors2.text.neutral.primary + } + val text = remember(full, subtitle.highlight, highlightColor) { + buildAnnotatedString { + append(full) + val start = full.lastIndexOf(subtitle.highlight) + if (start >= 0) { + addStyle(SpanStyle(color = highlightColor), start, start + subtitle.highlight.length) + } + } + } + Text( + text = text, + color = TangemTheme.colors2.text.neutral.tertiary, + style = TangemTheme.typography2.captionMedium12, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier, + ) +} + private fun ContentSubtitle.Direction.templateResId(): Int = when (this) { ContentSubtitle.Direction.TO -> R.string.transaction_history_to_inline_address ContentSubtitle.Direction.FROM -> R.string.transaction_history_from_inline_address diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionItemUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionItemUM.kt index 2debebbbee..1c3adfcd0e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionItemUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionItemUM.kt @@ -55,9 +55,16 @@ sealed interface TransactionItemUM { /** Subtitle variants for [Content] rows. */ @Immutable sealed interface ContentSubtitle { - /** Plain text — for types without a directly-displayable address (Operation, GaslessFee, ClaimRewards, etc.). */ + /** Plain text — for types without a displayable address (Operation, GaslessFee, ClaimRewards, etc.). */ data class Plain(val text: TextReference) : ContentSubtitle + /** + * Plain-text address subtitle without an identicon — renders "prefix:
", where [highlight] + * (a substring of the resolved [text]) is painted in the primary text color while the prefix stays + * tertiary. Used for contract / validator addresses, external swap addresses and yield "for:
". + */ + data class PlainAddress(val text: TextReference, val highlight: String) : ContentSubtitle + /** * External counterparty address — renders as "to/from: ". * Used for Transfer to/from external addresses. diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelGetSelectApprovalTypeParamsTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelGetSelectApprovalTypeParamsTest.kt index 4ccd0da81d..c140ebdd0a 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelGetSelectApprovalTypeParamsTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelGetSelectApprovalTypeParamsTest.kt @@ -2,6 +2,7 @@ package com.tangem.feature.swap.model import com.google.common.truth.Truth.assertThat import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ui.PermissionDataState @@ -41,7 +42,7 @@ internal class SwapModelGetSelectApprovalTypeParamsTest : SwapModelTestBase() { @Test fun `GIVEN provider state is not QuotesLoadedState THEN returns null`() { val provider = swapProvider() - val notLoaded: SwapState.EmptyAmountState = mockk(relaxed = true) + val notLoaded = SwapState.EmptyAmountState(zeroAmountEquivalent = TextReference.EMPTY) val model = createModel() model.dataState = model.dataState.copy( fromSwapCurrencyStatus = swapCurrencyStatus(), diff --git a/features/txhistory/impl/build.gradle.kts b/features/txhistory/impl/build.gradle.kts index 04d8147aa1..2e1f349d03 100644 --- a/features/txhistory/impl/build.gradle.kts +++ b/features/txhistory/impl/build.gradle.kts @@ -9,6 +9,12 @@ plugins { android { namespace = "com.tangem.features.txhistory.impl" + + packaging { + resources { + merges += "paymentrequest.proto" + } + } } dependencies { /* Project - API */ diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverter.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverter.kt index 30d4fa4243..afd4459fd5 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverter.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverter.kt @@ -83,7 +83,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = stringReference(type.name), iconRes = tx.directionalIcon(), - subtitle = ContentSubtitle.Plain(tx.extractSubtitleByAddressType()), + subtitle = tx.extractAddressSubtitle(), ) private fun swapContent(tx: TxInfo, uiStatus: TransactionItemUM.Content.Status): TransactionItemUM.Content = @@ -92,7 +92,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = tx.statusAwareTitle(R.string.common_swapping, R.string.common_swapped), iconRes = tx.directionalIcon(), - subtitle = ContentSubtitle.Plain(tx.extractSubtitleByAddressType()), + subtitle = tx.extractAddressSubtitle(), ) private fun transferContent(tx: TxInfo, uiStatus: TransactionItemUM.Content.Status): TransactionItemUM.Content { @@ -112,7 +112,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( rawAddress = counterpartyAddress, briefAddress = counterpartyAddress.toBriefAddressFormat(), ) - else -> ContentSubtitle.Plain(tx.extractSubtitleByAddressType()) + else -> tx.extractAddressSubtitle() } return buildContent( @@ -145,7 +145,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = resourceReference(R.string.yield_module_transaction_topup), iconRes = tx.directionalIcon(), - subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)), + subtitle = tx.yieldSupplySubtitle(currency, type), ) private fun yieldDeployContractContent( @@ -157,7 +157,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = resourceReference(R.string.yield_module_transaction_deploy_contract), iconRes = R.drawable.ic_doc_24, - subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)), + subtitle = tx.yieldSupplySubtitle(currency, type), ) private fun yieldInitializeTokenContent( @@ -169,7 +169,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = resourceReference(R.string.yield_module_transaction_initialize), iconRes = R.drawable.ic_gear_24, - subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)), + subtitle = tx.yieldSupplySubtitle(currency, type), ) private fun yieldReactivateTokenContent( @@ -181,7 +181,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = resourceReference(R.string.yield_module_transaction_reactivate), iconRes = R.drawable.ic_refresh_24, - subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)), + subtitle = tx.yieldSupplySubtitle(currency, type), ) private fun yieldSendContent( @@ -197,7 +197,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( resourceReference(R.string.common_transfer) }, iconRes = tx.directionalIcon(), - subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)), + subtitle = tx.yieldSupplySubtitle(currency, type), hideAmount = currency is CryptoCurrency.Token && !tx.isOutgoing, ) @@ -209,7 +209,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = resourceReference(R.string.transaction_history_operation), iconRes = tx.directionalIcon(), - subtitle = ContentSubtitle.Plain(tx.extractSubtitleByAddressType()), + subtitle = tx.extractAddressSubtitle(), ) private fun gaslessFeeContent(tx: TxInfo, uiStatus: TransactionItemUM.Content.Status): TransactionItemUM.Content = @@ -218,7 +218,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = resourceReference(R.string.gasless_transaction_fee), iconRes = tx.directionalIcon(), - subtitle = ContentSubtitle.Plain(tx.extractSubtitleByAddressType()), + subtitle = tx.extractAddressSubtitle(), ) private fun buildContent( @@ -283,15 +283,21 @@ private fun resolveOwnSubtitle( } } -private fun TxInfo.yieldSupplySubtitle(currency: CryptoCurrency, type: TransactionType.YieldSupply): TextReference { +private fun TxInfo.yieldSupplySubtitle(currency: CryptoCurrency, type: TransactionType.YieldSupply): ContentSubtitle { if (currency is CryptoCurrency.Coin) { return if (type is TransactionType.YieldSupply.Send) { - extractSubtitleByAddressType() + extractAddressSubtitle() } else { - resourceReference( + val briefAddress = type.address?.toBriefAddressFormat() + val text = resourceReference( R.string.transaction_history_transaction_for_address, - wrappedList(type.address?.toBriefAddressFormat().orEmpty()), + wrappedList(briefAddress.orEmpty()), ) + if (briefAddress.isNullOrEmpty()) { + ContentSubtitle.Plain(text) + } else { + ContentSubtitle.PlainAddress(text = text, highlight = briefAddress) + } } } return when (type) { @@ -304,15 +310,34 @@ private fun TxInfo.yieldSupplySubtitle(currency: CryptoCurrency, type: Transacti is TransactionType.YieldSupply.Send -> if (!isOutgoing && type.isYieldSupplyWithdraw) { amountSubtitle(currency, R.string.yield_module_transaction_exit_subtitle) } else { - extractSubtitleByAddressType() + extractAddressSubtitle() } - else -> extractSubtitleByAddressType() + else -> extractAddressSubtitle() } } -private fun TxInfo.amountSubtitle(currency: CryptoCurrency, @StringRes resId: Int): TextReference { +private fun TxInfo.amountSubtitle(currency: CryptoCurrency, @StringRes resId: Int): ContentSubtitle.Plain { val formatted = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } - return resourceReference(resId, wrappedList(formatted)) + return ContentSubtitle.Plain(resourceReference(resId, wrappedList(formatted))) +} + +private fun TxInfo.extractAddressSubtitle(): ContentSubtitle { + val text = extractSubtitleByAddressType() + val highlight = interactionAddressType.highlightableBriefAddress() + return if (highlight == null) { + ContentSubtitle.Plain(text) + } else { + ContentSubtitle.PlainAddress(text = text, highlight = highlight) + } +} + +private fun TxInfo.InteractionAddressType?.highlightableBriefAddress(): String? = when (this) { + is TxInfo.InteractionAddressType.Contract -> address.toBriefAddressFormat() + is TxInfo.InteractionAddressType.User -> address.toBriefAddressFormat() + is TxInfo.InteractionAddressType.Validator -> address.toBriefAddressFormat() + is TxInfo.InteractionAddressType.Multiple, + null, + -> null } private fun TxInfo.extractSubtitleByAddressType(): TextReference = diff --git a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverterTest.kt b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverterTest.kt index 1b8e49f8db..ae76d9a485 100644 --- a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverterTest.kt +++ b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverterTest.kt @@ -341,7 +341,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest { } @Test - fun `GIVEN Transfer with non-User interaction WHEN convert THEN Plain subtitle`() { + fun `GIVEN Transfer with non-User interaction WHEN convert THEN PlainAddress subtitle`() { val tx = txInfo( type = TransactionType.Transfer, isOutgoing = true, @@ -350,7 +350,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest { val result = coinConverter.convert(tx) as TransactionItemUM.Content - assertThat(result.subtitle).isInstanceOf(ContentSubtitle.Plain::class.java) + assertThat(result.subtitle).isInstanceOf(ContentSubtitle.PlainAddress::class.java) assertThat(result.title).isEqualTo(resRef(R.string.common_sent)) } @@ -575,7 +575,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest { val result = coinConverter.convert(tx) as TransactionItemUM.Content - val subtitle = result.subtitle as ContentSubtitle.Plain + val subtitle = result.subtitle as ContentSubtitle.PlainAddress val res = subtitle.text as TextReference.Res assertThat(res.id).isEqualTo(R.string.transaction_history_contract_address) } @@ -623,7 +623,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest { val result = coinConverter.convert(tx) as TransactionItemUM.Content - val subtitle = result.subtitle as ContentSubtitle.Plain + val subtitle = result.subtitle as ContentSubtitle.PlainAddress val res = subtitle.text as TextReference.Res assertThat(res.id).isEqualTo(R.string.transaction_history_transaction_validator) }