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