Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-25 14:44:18 +03:00
parent 51e650c360
commit 391e0562cc
13 changed files with 519 additions and 115 deletions

View file

@ -40,6 +40,8 @@ import com.tangem.core.ui.components.transactions.state.TransactionItemUM
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Direction import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Direction
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.ContentSubtitle import com.tangem.core.ui.components.transactions.state.TransactionItemUM.ContentSubtitle
import com.tangem.core.ui.components.transactions.state.TxIcon
import com.tangem.core.ui.components.transactions.state.asImageVector
import com.tangem.core.ui.ds.image.TangemDeviceIcon import com.tangem.core.ui.ds.image.TangemDeviceIcon
import com.tangem.core.ui.ds.row.TangemRowContainer import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId import com.tangem.core.ui.ds.row.TangemRowLayoutId
@ -50,6 +52,12 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_down_20
import com.tangem.core.ui.res.generated.icons.ic_arrow_swap_horizontal_20
import com.tangem.core.ui.res.generated.icons.ic_arrow_up_20
import com.tangem.core.ui.res.generated.icons.ic_card_20
import com.tangem.core.ui.res.generated.icons.ic_cross_20
import com.tangem.core.ui.test.TransactionHistoryItemTestTags import com.tangem.core.ui.test.TransactionHistoryItemTestTags
@Composable @Composable
@ -86,7 +94,7 @@ private fun ContentItem(state: TransactionItemUM.Content, isBalanceHidden: Boole
), ),
) { ) {
StatusCircle( StatusCircle(
iconRes = state.iconRes, icon = state.icon,
status = state.status, status = state.status,
modifier = Modifier modifier = Modifier
.layoutId(TangemRowLayoutId.HEAD) .layoutId(TangemRowLayoutId.HEAD)
@ -166,7 +174,7 @@ private fun WarningLine(warning: TextReference, modifier: Modifier = Modifier) {
// region Status circle // region Status circle
@Composable @Composable
private fun StatusCircle(iconRes: Int, status: Status, modifier: Modifier = Modifier) { private fun StatusCircle(icon: TxIcon, status: Status, modifier: Modifier = Modifier) {
Box( Box(
modifier = modifier.background( modifier = modifier.background(
color = status.backgroundColor, color = status.backgroundColor,
@ -174,7 +182,7 @@ private fun StatusCircle(iconRes: Int, status: Status, modifier: Modifier = Modi
), ),
) { ) {
Icon( Icon(
painter = painterResource(iconRes), imageVector = icon.asImageVector(),
contentDescription = null, contentDescription = null,
tint = status.iconTint, tint = status.iconTint,
modifier = Modifier modifier = Modifier
@ -396,7 +404,7 @@ private fun String.stripLeadingSign(): String = when {
@Suppress("LongParameterList") @Suppress("LongParameterList")
private fun previewContent( private fun previewContent(
txHash: String, txHash: String,
iconRes: Int, icon: TxIcon,
direction: Direction, direction: Direction,
status: Status, status: Status,
title: String, title: String,
@ -411,7 +419,7 @@ private fun previewContent(
status = status, status = status,
direction = direction, direction = direction,
onClick = {}, onClick = {},
iconRes = iconRes, icon = icon,
title = stringReference(title), title = stringReference(title),
subtitle = ContentSubtitle.Plain(stringReference(subtitle)), subtitle = ContentSubtitle.Plain(stringReference(subtitle)),
timestamp = 0L, timestamp = 0L,
@ -438,7 +446,7 @@ private fun Preview_TransactionItem_Receive() {
items = listOf( items = listOf(
previewContent( previewContent(
txHash = "rcv-c", txHash = "rcv-c",
iconRes = R.drawable.ic_arrow_down_24, icon = TxIcon.Vector(Icons.ic_arrow_down_20),
direction = Direction.INCOMING, direction = Direction.INCOMING,
status = Status.Confirmed, status = Status.Confirmed,
title = "Received", title = "Received",
@ -447,7 +455,7 @@ private fun Preview_TransactionItem_Receive() {
), ),
previewContent( previewContent(
txHash = "rcv-u", txHash = "rcv-u",
iconRes = R.drawable.ic_arrow_down_24, icon = TxIcon.Vector(Icons.ic_arrow_down_20),
direction = Direction.INCOMING, direction = Direction.INCOMING,
status = Status.Unconfirmed, status = Status.Unconfirmed,
title = "Receiving", title = "Receiving",
@ -456,7 +464,7 @@ private fun Preview_TransactionItem_Receive() {
), ),
previewContent( previewContent(
txHash = "rcv-f", txHash = "rcv-f",
iconRes = R.drawable.ic_close_24, icon = TxIcon.Vector(Icons.ic_cross_20),
direction = Direction.INCOMING, direction = Direction.INCOMING,
status = Status.Failed, status = Status.Failed,
title = "Receiving failed", title = "Receiving failed",
@ -477,7 +485,7 @@ private fun Preview_TransactionItem_Send() {
items = listOf( items = listOf(
previewContent( previewContent(
txHash = "snd-c", txHash = "snd-c",
iconRes = R.drawable.ic_arrow_up_24, icon = TxIcon.Vector(Icons.ic_arrow_up_20),
direction = Direction.OUTGOING, direction = Direction.OUTGOING,
status = Status.Confirmed, status = Status.Confirmed,
title = "Sent", title = "Sent",
@ -486,7 +494,7 @@ private fun Preview_TransactionItem_Send() {
), ),
previewContent( previewContent(
txHash = "snd-u", txHash = "snd-u",
iconRes = R.drawable.ic_arrow_up_24, icon = TxIcon.Vector(Icons.ic_arrow_up_20),
direction = Direction.OUTGOING, direction = Direction.OUTGOING,
status = Status.Unconfirmed, status = Status.Unconfirmed,
title = "Sending", title = "Sending",
@ -495,7 +503,7 @@ private fun Preview_TransactionItem_Send() {
), ),
previewContent( previewContent(
txHash = "snd-f", txHash = "snd-f",
iconRes = R.drawable.ic_close_24, icon = TxIcon.Vector(Icons.ic_cross_20),
direction = Direction.OUTGOING, direction = Direction.OUTGOING,
status = Status.Failed, status = Status.Failed,
title = "Sending failed", title = "Sending failed",
@ -516,7 +524,7 @@ private fun Preview_TransactionItem_Swap() {
items = listOf( items = listOf(
previewContent( previewContent(
txHash = "swp-c", txHash = "swp-c",
iconRes = R.drawable.ic_exchange_vertical_24, icon = TxIcon.Vector(Icons.ic_arrow_swap_horizontal_20),
direction = Direction.INCOMING, direction = Direction.INCOMING,
status = Status.Confirmed, status = Status.Confirmed,
title = "Swapped", title = "Swapped",
@ -525,7 +533,7 @@ private fun Preview_TransactionItem_Swap() {
), ),
previewContent( previewContent(
txHash = "swp-u", txHash = "swp-u",
iconRes = R.drawable.ic_exchange_vertical_24, icon = TxIcon.Vector(Icons.ic_arrow_swap_horizontal_20),
direction = Direction.INCOMING, direction = Direction.INCOMING,
status = Status.Unconfirmed, status = Status.Unconfirmed,
title = "Swapping", title = "Swapping",
@ -534,7 +542,7 @@ private fun Preview_TransactionItem_Swap() {
), ),
previewContent( previewContent(
txHash = "swp-f", txHash = "swp-f",
iconRes = R.drawable.ic_close_24, icon = TxIcon.Vector(Icons.ic_cross_20),
direction = Direction.INCOMING, direction = Direction.INCOMING,
status = Status.Failed, status = Status.Failed,
title = "Swapping failed", title = "Swapping failed",
@ -561,7 +569,7 @@ private fun Preview_TransactionItem_Express() {
status = Status.Unconfirmed, status = Status.Unconfirmed,
direction = Direction.OUTGOING, direction = Direction.OUTGOING,
onClick = {}, onClick = {},
iconRes = R.drawable.ic_exchange_vertical_24, icon = TxIcon.Vector(Icons.ic_arrow_swap_horizontal_20),
title = stringReference("Swapping"), title = stringReference("Swapping"),
subtitle = ContentSubtitle.Asset( subtitle = ContentSubtitle.Asset(
direction = ContentSubtitle.Direction.TO, direction = ContentSubtitle.Direction.TO,
@ -584,7 +592,7 @@ private fun Preview_TransactionItem_Express() {
status = Status.Confirmed, status = Status.Confirmed,
direction = Direction.INCOMING, direction = Direction.INCOMING,
onClick = {}, onClick = {},
iconRes = R.drawable.ic_tangem_card_24, icon = TxIcon.Vector(Icons.ic_card_20),
title = stringReference("Topped up"), title = stringReference("Topped up"),
subtitle = ContentSubtitle.Asset( subtitle = ContentSubtitle.Asset(
direction = ContentSubtitle.Direction.FROM, direction = ContentSubtitle.Direction.FROM,
@ -601,7 +609,7 @@ private fun Preview_TransactionItem_Express() {
status = Status.Failed, status = Status.Failed,
direction = Direction.INCOMING, direction = Direction.INCOMING,
onClick = {}, onClick = {},
iconRes = R.drawable.ic_tangem_card_24, icon = TxIcon.Vector(Icons.ic_card_20),
title = stringReference("Top up failed"), title = stringReference("Top up failed"),
subtitle = ContentSubtitle.Asset( subtitle = ContentSubtitle.Asset(
direction = ContentSubtitle.Direction.FROM, direction = ContentSubtitle.Direction.FROM,

View file

@ -1,8 +1,11 @@
package com.tangem.core.ui.components.transactions.state package com.tangem.core.ui.components.transactions.state
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.ds.image.DeviceIconUM
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
@ -35,7 +38,7 @@ sealed interface TransactionItemUM {
val status: Status, val status: Status,
val direction: Direction, val direction: Direction,
val onClick: () -> Unit, val onClick: () -> Unit,
@DrawableRes val iconRes: Int, val icon: TxIcon,
val title: TextReference, val title: TextReference,
val subtitle: ContentSubtitle, val subtitle: ContentSubtitle,
val timestamp: Long, val timestamp: Long,
@ -153,4 +156,40 @@ sealed interface TransactionItemUM {
data class Loading(override val txHash: String) : TransactionItemUM data class Loading(override val txHash: String) : TransactionItemUM
data class Locked(override val txHash: String) : TransactionItemUM data class Locked(override val txHash: String) : TransactionItemUM
}
/**
* Status-circle icon for a [TransactionItemUM.Content] row.
*
* [Vector] holds a design-system [ImageVector] (`Icons.ic_*`) the migration target; [Res] holds a `@DrawableRes`
* XML fallback for transaction types that don't have a DS3 icon yet (e.g. gear / claim rewards).
*/
@Immutable
sealed interface TxIcon {
/** Design-system vector icon (`Icons.ic_*`). */
data class Vector(val imageVector: ImageVector) : TxIcon
/**
* Legacy XML drawable fallback.
*
* Forced temporary bridge: a few transaction types still map to legacy XML drawables that have no DS3
* `Icons.ic_*` counterpart yet (e.g. gear / claim rewards), while some DS3 icons in turn have no legacy
* drawable. Once the DS team ships the full icon set, every call site switches to [Vector] and this whole
* subclass is removed.
*/
@Deprecated(
message = "Temporary fallback for tx types missing a DS3 icon. Migrate to TxIcon.Vector once the DS team " +
"ships the remaining Icons.ic_* glyphs; this subclass will then be removed.",
replaceWith = ReplaceWith("TxIcon.Vector(imageVector)"),
)
data class Res(@DrawableRes val resId: Int) : TxIcon
}
/** Resolves a [TxIcon] to the [ImageVector] to draw: the DS3 vector directly, or the legacy XML drawable parsed. */
@Suppress("DEPRECATION")
@Composable
fun TxIcon.asImageVector(): ImageVector = when (this) {
is TxIcon.Vector -> imageVector
is TxIcon.Res -> ImageVector.vectorResource(resId)
} }

View file

@ -74,6 +74,13 @@ inline val TxHistoryInfo.explorerHash: String?
is ExpressTx -> matchHash is ExpressTx -> matchHash
} }
/** Human-meaningful transaction id to copy/display: the on-chain hash for an [OnChainTx], the express deal id otherwise. */
inline val TxHistoryInfo.idToCopy: String
get() = when (this) {
is OnChainTx.BSDK -> txInfo.txHash
is ExpressTx -> txId
}
/** /**
* A history row backed by an express operation. It is a thin wrapper over the standalone express * A history row backed by an express operation. It is a thin wrapper over the standalone express
* model ([ExchangeTransaction] / [OnrampTransaction]), adding only the history-view concerns: * model ([ExchangeTransaction] / [OnrampTransaction]), adding only the history-view concerns:

View file

@ -7,11 +7,15 @@ import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToI
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.ContentSubtitle import com.tangem.core.ui.components.transactions.state.TransactionItemUM.ContentSubtitle
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.ContentSubtitle.Direction as SubtitleDirection import com.tangem.core.ui.components.transactions.state.TransactionItemUM.ContentSubtitle.Direction as SubtitleDirection
import com.tangem.core.ui.components.transactions.state.TxIcon
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_swap_horizontal_20
import com.tangem.core.ui.res.generated.icons.ic_card_20
import com.tangem.core.ui.utils.toTimeFormat import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.domain.express.models.ExpressExchangeStatus import com.tangem.domain.express.models.ExpressExchangeStatus
import com.tangem.domain.express.models.ExpressOnrampStatus import com.tangem.domain.express.models.ExpressOnrampStatus
@ -63,11 +67,11 @@ internal class ExpressTxToTransactionItemUMConverter(
status = status, status = status,
amount = formatAmount(viewedAmount, prefix), amount = formatAmount(viewedAmount, prefix),
direction = if (swap.isOutgoing) RowDirection.OUTGOING else RowDirection.INCOMING, direction = if (swap.isOutgoing) RowDirection.OUTGOING else RowDirection.INCOMING,
iconRes = R.drawable.ic_exchange_vertical_24, icon = TxIcon.Vector(Icons.ic_arrow_swap_horizontal_20),
title = swapTitle(status), title = swapTitle(status),
subtitle = ContentSubtitle.Asset( subtitle = ContentSubtitle.Asset(
direction = if (swap.isOutgoing) SubtitleDirection.TO else SubtitleDirection.FROM, direction = if (swap.isOutgoing) SubtitleDirection.TO else SubtitleDirection.FROM,
symbol = counterparty.cryptoCurrency?.symbol ?: counterparty.id.networkId, symbol = counterparty.displaySymbol,
icon = counterparty.cryptoCurrency?.let(iconStateConverter::convert), icon = counterparty.cryptoCurrency?.let(iconStateConverter::convert),
), ),
warning = swapWarning(swap), warning = swapWarning(swap),
@ -86,7 +90,7 @@ internal class ExpressTxToTransactionItemUMConverter(
status = status, status = status,
amount = formatAmount(onramp.tx.toAsset.amount, prefix), amount = formatAmount(onramp.tx.toAsset.amount, prefix),
direction = RowDirection.INCOMING, direction = RowDirection.INCOMING,
iconRes = R.drawable.ic_tangem_card_24, icon = TxIcon.Vector(Icons.ic_card_20),
title = onrampTitle(status), title = onrampTitle(status),
subtitle = ContentSubtitle.Asset( subtitle = ContentSubtitle.Asset(
direction = SubtitleDirection.FROM, direction = SubtitleDirection.FROM,
@ -106,7 +110,7 @@ internal class ExpressTxToTransactionItemUMConverter(
status: Status, status: Status,
amount: String?, amount: String?,
direction: RowDirection, direction: RowDirection,
iconRes: Int, icon: TxIcon,
title: TextReference, title: TextReference,
subtitle: ContentSubtitle, subtitle: ContentSubtitle,
warning: TextReference?, warning: TextReference?,
@ -120,7 +124,7 @@ internal class ExpressTxToTransactionItemUMConverter(
status = status, status = status,
direction = direction, direction = direction,
onClick = { txHistoryUiActions.onTransactionClick(tx) }, onClick = { txHistoryUiActions.onTransactionClick(tx) },
iconRes = iconRes, icon = icon,
title = title, title = title,
subtitle = subtitle, subtitle = subtitle,
timestamp = tx.timestampMillis, timestamp = tx.timestampMillis,

View file

@ -0,0 +1,49 @@
package com.tangem.features.txhistory.converter
import androidx.annotation.StringRes
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.express.models.ExpressTransactionAsset
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType
import com.tangem.features.txhistory.impl.R
// region Status helpers
/** Maps the domain [TxInfo.TransactionStatus] to the UI [Status] bucket that drives row title/icon/amount colors. */
internal fun TxInfo.TransactionStatus.toUiStatus(): Status = when (this) {
TxInfo.TransactionStatus.Confirmed -> Status.Confirmed
TxInfo.TransactionStatus.Failed -> Status.Failed
TxInfo.TransactionStatus.Unconfirmed -> Status.Unconfirmed
}
/**
* Status-aware action title: the [confirmed] label once settled, the [pending] label while in flight, and the
* "{pending} failed" template on failure.
*/
internal fun Status.statusAwareTitle(@StringRes pending: Int, @StringRes confirmed: Int): TextReference = when (this) {
is Status.Failed -> resourceReference(R.string.common_action_failed, wrappedList(resourceReference(pending)))
is Status.Unconfirmed -> resourceReference(pending)
is Status.Confirmed -> resourceReference(confirmed)
}
/** [statusAwareTitle] keyed off an on-chain [TxInfo]'s status. */
internal fun TxInfo.statusAwareTitle(@StringRes pending: Int, @StringRes confirmed: Int): TextReference =
status.toUiStatus().statusAwareTitle(pending, confirmed)
// endregion
// region Express asset helpers
/** Ticker shown for an express leg: the resolved currency symbol, falling back to the network id while unresolved. */
internal val ExpressTransactionAsset.displaySymbol: String
get() = cryptoCurrency?.symbol ?: id.networkId
/** Fiat currency code of an [Amount], falling back to its symbol when the amount is not a fiat type. */
internal val Amount.fiatCode: String
get() = (type as? AmountType.FiatType)?.code ?: currencySymbol
// endregion

View file

@ -6,13 +6,21 @@ import com.tangem.common.ui.account.getUiColor
import com.tangem.common.ui.account.toUM import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status
import com.tangem.core.ui.components.transactions.state.TxIcon
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_down_20
import com.tangem.core.ui.res.generated.icons.ic_arrow_swap_horizontal_20
import com.tangem.core.ui.res.generated.icons.ic_arrow_up_20
import com.tangem.core.ui.res.generated.icons.ic_card_20
import com.tangem.core.ui.res.generated.icons.ic_copy_24
import com.tangem.core.ui.res.generated.icons.ic_globe_24
import com.tangem.core.ui.res.generated.icons.ic_share_android_24
import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.express.models.ExchangeTransaction import com.tangem.domain.express.models.ExchangeTransaction
import com.tangem.domain.express.models.ExpressExchangeStatus import com.tangem.domain.express.models.ExpressExchangeStatus
@ -24,7 +32,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.network.TxInfo.TransactionType import com.tangem.domain.models.network.TxInfo.TransactionType
import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType
import com.tangem.domain.txhistory.model.ExpressTx import com.tangem.domain.txhistory.model.ExpressTx
import com.tangem.domain.txhistory.model.OnChainTx import com.tangem.domain.txhistory.model.OnChainTx
import com.tangem.domain.txhistory.model.TxHistoryInfo import com.tangem.domain.txhistory.model.TxHistoryInfo
@ -57,6 +64,9 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverter(
private val currency: CryptoCurrency, private val currency: CryptoCurrency,
private val onCopyAddress: (String) -> Unit, private val onCopyAddress: (String) -> Unit,
private val onGoToProvider: (String) -> Unit, private val onGoToProvider: (String) -> Unit,
private val onCopyTxId: (() -> Unit)? = null,
private val onShare: (() -> Unit)? = null,
private val onExplore: (() -> Unit)? = null,
private val lookup: TxHistoryLookupContext = TxHistoryLookupContext( private val lookup: TxHistoryLookupContext = TxHistoryLookupContext(
ownAccountByNetwork = emptyMap(), ownAccountByNetwork = emptyMap(),
isAccountsModeEnabled = false, isAccountsModeEnabled = false,
@ -94,12 +104,49 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverter(
) )
private fun TxInfo.toHeaderUM(): TxHistoryDetailsUM.HeaderUM = TxHistoryDetailsUM.HeaderUM( private fun TxInfo.toHeaderUM(): TxHistoryDetailsUM.HeaderUM = TxHistoryDetailsUM.HeaderUM(
iconRes = headerIcon(), icon = headerIcon(),
status = status.toUiStatus(), status = status.toUiStatus(),
title = headerTitle(), title = headerTitle(),
subtitle = headerSubtitle(timestampInMillis), subtitle = headerSubtitle(timestampInMillis),
menu = buildMenu(),
) )
/**
* Header overflow context menu, shared by all transaction types. Each row is dropped when its action is absent:
* "Transaction ID" (copy; dropped when [onCopyTxId] is `null` no id to copy), "Share" (dropped when [onShare] is
* `null`) and "Explore" (dropped when [onExplore] is `null`). An empty list leaves the header with no "•••" button.
* Repeat / Hide are not part of this iteration.
*/
private fun buildMenu(): ImmutableList<TxHistoryDetailsUM.MenuItemUM> = buildList {
onCopyTxId?.let { copy ->
add(
TxHistoryDetailsUM.MenuItemUM(
icon = Icons.ic_copy_24,
title = resourceReference(R.string.common_transaction_id),
onClick = copy,
),
)
}
onShare?.let { share ->
add(
TxHistoryDetailsUM.MenuItemUM(
icon = Icons.ic_share_android_24,
title = resourceReference(R.string.common_share),
onClick = share,
),
)
}
onExplore?.let { explore ->
add(
TxHistoryDetailsUM.MenuItemUM(
icon = Icons.ic_globe_24,
title = resourceReference(R.string.common_explore),
onClick = explore,
),
)
}
}.toImmutableList()
private fun TxInfo.toAmountBlockUM(): TxHistoryDetailsUM.AmountBlockUM = TxHistoryDetailsUM.AmountBlockUM( private fun TxInfo.toAmountBlockUM(): TxHistoryDetailsUM.AmountBlockUM = TxHistoryDetailsUM.AmountBlockUM(
currencyIcon = iconStateConverter.convert(currency), currencyIcon = iconStateConverter.convert(currency),
amount = stringReference(signedAmount(currency)), amount = stringReference(signedAmount(currency)),
@ -163,10 +210,11 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverter(
val toOwner = resolveLegOwner(swap.tx.payoutAddress, swap.tx.toAsset.cryptoCurrency) val toOwner = resolveLegOwner(swap.tx.payoutAddress, swap.tx.toAsset.cryptoCurrency)
return TxHistoryDetailsUM.TwoAssets( return TxHistoryDetailsUM.TwoAssets(
header = TxHistoryDetailsUM.HeaderUM( header = TxHistoryDetailsUM.HeaderUM(
iconRes = R.drawable.ic_exchange_vertical_24, icon = TxIcon.Vector(Icons.ic_arrow_swap_horizontal_20),
status = status, status = status,
title = status.statusAwareTitle(R.string.common_swapping, R.string.common_swapped), title = status.statusAwareTitle(R.string.common_swapping, R.string.common_swapped),
subtitle = headerSubtitle(swap.timestampMillis), subtitle = headerSubtitle(swap.timestampMillis),
menu = buildMenu(),
), ),
from = swap.tx.fromAsset.toAssetUM( from = swap.tx.fromAsset.toAssetUM(
label = ownerLabel(fromOwner, fallback = R.string.swapping_from_title_v2, owned = R.string.common_from), label = ownerLabel(fromOwner, fallback = R.string.swapping_from_title_v2, owned = R.string.common_from),
@ -191,13 +239,14 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverter(
val toOwner = resolveLegOwner(onramp.tx.payoutAddress, onramp.tx.toAsset.cryptoCurrency) val toOwner = resolveLegOwner(onramp.tx.payoutAddress, onramp.tx.toAsset.cryptoCurrency)
return TxHistoryDetailsUM.TwoAssets( return TxHistoryDetailsUM.TwoAssets(
header = TxHistoryDetailsUM.HeaderUM( header = TxHistoryDetailsUM.HeaderUM(
iconRes = R.drawable.ic_tangem_card_24, icon = TxIcon.Vector(Icons.ic_card_20),
status = status, status = status,
title = status.statusAwareTitle( title = status.statusAwareTitle(
R.string.tx_history_onramp_top_up, R.string.tx_history_onramp_top_up,
R.string.tx_history_onramp_topped_up, R.string.tx_history_onramp_topped_up,
), ),
subtitle = headerSubtitle(onramp.timestampMillis), subtitle = headerSubtitle(onramp.timestampMillis),
menu = buildMenu(),
), ),
from = onramp.tx.fromFiat.toFiatAssetUM( from = onramp.tx.fromFiat.toFiatAssetUM(
// The fiat side was paid from a card, not a portfolio address — no owner to resolve. // The fiat side was paid from a card, not a portfolio address — no owner to resolve.
@ -269,7 +318,7 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverter(
sign: String, sign: String,
isFaded: Boolean, isFaded: Boolean,
): TxHistoryDetailsUM.AssetUM { ): TxHistoryDetailsUM.AssetUM {
val symbol = cryptoCurrency?.symbol ?: id.networkId val symbol = displaySymbol
val formatted = amount.format { crypto( val formatted = amount.format { crypto(
symbol = symbol, symbol = symbol,
decimals = decimals, decimals = decimals,
@ -289,7 +338,7 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverter(
* nor the `~` estimate so only the value is shown. Fiat has no `CryptoCurrency`, so it also has no icon. * nor the `~` estimate so only the value is shown. Fiat has no `CryptoCurrency`, so it also has no icon.
*/ */
private fun Amount.toFiatAssetUM(label: TextReference, isFaded: Boolean): TxHistoryDetailsUM.AssetUM { private fun Amount.toFiatAssetUM(label: TextReference, isFaded: Boolean): TxHistoryDetailsUM.AssetUM {
val code = (type as? AmountType.FiatType)?.code ?: currencySymbol val code = fiatCode
val formatted = (value ?: BigDecimal.ZERO) val formatted = (value ?: BigDecimal.ZERO)
.format { fiat(fiatCurrencyCode = code, fiatCurrencySymbol = currencySymbol) } .format { fiat(fiatCurrencyCode = code, fiatCurrencySymbol = currencySymbol) }
return TxHistoryDetailsUM.AssetUM( return TxHistoryDetailsUM.AssetUM(
@ -421,12 +470,6 @@ private fun verificationBanner() = TxHistoryDetailsUM.StatusBannerUM(
isLoading = false, isLoading = false,
) )
private fun Status.statusAwareTitle(@StringRes pending: Int, @StringRes confirmed: Int): TextReference = when (this) {
is Status.Failed -> resourceReference(R.string.common_action_failed, wrappedList(resourceReference(pending)))
is Status.Unconfirmed -> resourceReference(pending)
is Status.Confirmed -> resourceReference(confirmed)
}
// endregion // endregion
// region Info rows (provider / rate / network fee) // region Info rows (provider / rate / network fee)
@ -489,8 +532,8 @@ private fun ExchangeTransaction.swapRateRow(): TxHistoryDetailsUM.InfoRowUM? {
val fromAmount = fromAsset.amount.takeIfPositive() ?: return null val fromAmount = fromAsset.amount.takeIfPositive() ?: return null
val toAmount = toAsset.amount.takeIfPositive() ?: return null val toAmount = toAsset.amount.takeIfPositive() ?: return null
val rate = toAmount.divide(fromAmount, rateScale(toAsset.decimals), RoundingMode.HALF_UP) val rate = toAmount.divide(fromAmount, rateScale(toAsset.decimals), RoundingMode.HALF_UP)
val baseSymbol = fromAsset.cryptoCurrency?.symbol ?: fromAsset.id.networkId val baseSymbol = fromAsset.displaySymbol
val quoteSymbol = toAsset.cryptoCurrency?.symbol ?: toAsset.id.networkId val quoteSymbol = toAsset.displaySymbol
val value = rateText( val value = rateText(
base = oneOf(baseSymbol), base = oneOf(baseSymbol),
quote = rate.format { crypto(symbol = quoteSymbol, decimals = toAsset.decimals, ignoreSymbolPosition = true) }, quote = rate.format { crypto(symbol = quoteSymbol, decimals = toAsset.decimals, ignoreSymbolPosition = true) },
@ -508,8 +551,8 @@ private fun OnrampTransaction.onrampRateRow(): TxHistoryDetailsUM.InfoRowUM? {
val cryptoReceived = toAsset.amount.takeIfPositive() ?: return null val cryptoReceived = toAsset.amount.takeIfPositive() ?: return null
// Divide at full precision; the fiat formatter then rounds the rate to the currency's display scale. // Divide at full precision; the fiat formatter then rounds the rate to the currency's display scale.
val rate = fiatPaid.divide(cryptoReceived, RATE_MAX_DECIMALS, RoundingMode.HALF_UP) val rate = fiatPaid.divide(cryptoReceived, RATE_MAX_DECIMALS, RoundingMode.HALF_UP)
val cryptoSymbol = toAsset.cryptoCurrency?.symbol ?: toAsset.id.networkId val cryptoSymbol = toAsset.displaySymbol
val fiatCode = (fromFiat.type as? AmountType.FiatType)?.code ?: fromFiat.currencySymbol val fiatCode = fromFiat.fiatCode
val value = rateText( val value = rateText(
base = oneOf(cryptoSymbol), base = oneOf(cryptoSymbol),
quote = rate.format { fiat(fiatCurrencyCode = fiatCode, fiatCurrencySymbol = fromFiat.currencySymbol) }, quote = rate.format { fiat(fiatCurrencyCode = fiatCode, fiatCurrencySymbol = fromFiat.currencySymbol) },
@ -576,9 +619,9 @@ private fun TxInfo.signedAmount(currency: CryptoCurrency): String {
// region Header building helpers // region Header building helpers
/** Type glyph. Unlike the history list, the failed state keeps the type glyph (only the color changes). */ /** Type glyph. Unlike the history list, the failed state keeps the type glyph (only the color changes). */
private fun TxInfo.headerIcon(): Int = when (type) { private fun TxInfo.headerIcon(): TxIcon = when (type) {
is TransactionType.Swap -> R.drawable.ic_exchange_vertical_24 is TransactionType.Swap -> TxIcon.Vector(Icons.ic_arrow_swap_horizontal_20)
else -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24 else -> TxIcon.Vector(if (isOutgoing) Icons.ic_arrow_up_20 else Icons.ic_arrow_down_20)
} }
private fun headerSubtitle(timestampMillis: Long): TextReference { private fun headerSubtitle(timestampMillis: Long): TextReference {
@ -588,17 +631,4 @@ private fun headerSubtitle(timestampMillis: Long): TextReference {
return stringReference("$date, $time") return stringReference("$date, $time")
} }
private fun TxInfo.statusAwareTitle(@StringRes pending: Int, @StringRes confirmed: Int): TextReference = when (status) {
is TxInfo.TransactionStatus.Failed ->
resourceReference(R.string.common_action_failed, wrappedList(resourceReference(pending)))
is TxInfo.TransactionStatus.Unconfirmed -> resourceReference(pending)
is TxInfo.TransactionStatus.Confirmed -> resourceReference(confirmed)
}
private fun TxInfo.TransactionStatus.toUiStatus(): Status = when (this) {
TxInfo.TransactionStatus.Confirmed -> Status.Confirmed
TxInfo.TransactionStatus.Failed -> Status.Failed
TxInfo.TransactionStatus.Unconfirmed -> Status.Unconfirmed
}
// endregion // endregion

View file

@ -6,10 +6,17 @@ import com.tangem.common.ui.account.getUiColor
import com.tangem.common.ui.account.toUM import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.components.transactions.state.TransactionItemUM import com.tangem.core.ui.components.transactions.state.TransactionItemUM
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.ContentSubtitle import com.tangem.core.ui.components.transactions.state.TransactionItemUM.ContentSubtitle
import com.tangem.core.ui.components.transactions.state.TxIcon
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_down_20
import com.tangem.core.ui.res.generated.icons.ic_arrow_refresh_20
import com.tangem.core.ui.res.generated.icons.ic_arrow_up_20
import com.tangem.core.ui.res.generated.icons.ic_cross_20
import com.tangem.core.ui.res.generated.icons.ic_document_20
import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.toTimeFormat import com.tangem.core.ui.utils.toTimeFormat
@ -85,7 +92,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
tx = tx, tx = tx,
uiStatus = uiStatus, uiStatus = uiStatus,
title = stringReference(type.name), title = stringReference(type.name),
iconRes = tx.directionalIcon(), icon = tx.directionalIcon(),
subtitle = tx.extractAddressSubtitle(), subtitle = tx.extractAddressSubtitle(),
) )
@ -94,7 +101,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
tx = tx, tx = tx,
uiStatus = uiStatus, uiStatus = uiStatus,
title = tx.statusAwareTitle(R.string.common_swapping, R.string.common_swapped), title = tx.statusAwareTitle(R.string.common_swapping, R.string.common_swapped),
iconRes = tx.directionalIcon(), icon = tx.directionalIcon(),
subtitle = tx.extractAddressSubtitle(), subtitle = tx.extractAddressSubtitle(),
) )
@ -129,7 +136,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
tx = tx, tx = tx,
uiStatus = uiStatus, uiStatus = uiStatus,
title = title, title = title,
iconRes = tx.directionalIcon(), icon = tx.directionalIcon(),
subtitle = subtitle, subtitle = subtitle,
) )
} }
@ -142,7 +149,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
pending = R.string.transaction_history_claiming_reward, pending = R.string.transaction_history_claiming_reward,
confirmed = R.string.transaction_history_staking_reward, confirmed = R.string.transaction_history_staking_reward,
), ),
iconRes = R.drawable.ic_transaction_history_claim_rewards_24, icon = TxIcon.Res(R.drawable.ic_transaction_history_claim_rewards_24),
subtitle = ContentSubtitle.Plain(resourceReference(R.string.transaction_history_earned_from_stake)), subtitle = ContentSubtitle.Plain(resourceReference(R.string.transaction_history_earned_from_stake)),
) )
@ -154,7 +161,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
tx = tx, tx = tx,
uiStatus = uiStatus, uiStatus = uiStatus,
title = resourceReference(R.string.yield_module_transaction_topup), title = resourceReference(R.string.yield_module_transaction_topup),
iconRes = tx.directionalIcon(), icon = tx.directionalIcon(),
subtitle = tx.yieldSupplySubtitle(currency, type), subtitle = tx.yieldSupplySubtitle(currency, type),
) )
@ -166,7 +173,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
tx = tx, tx = tx,
uiStatus = uiStatus, uiStatus = uiStatus,
title = resourceReference(R.string.yield_module_transaction_deploy_contract), title = resourceReference(R.string.yield_module_transaction_deploy_contract),
iconRes = R.drawable.ic_doc_24, icon = TxIcon.Vector(Icons.ic_document_20),
subtitle = tx.yieldSupplySubtitle(currency, type), subtitle = tx.yieldSupplySubtitle(currency, type),
) )
@ -178,7 +185,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
tx = tx, tx = tx,
uiStatus = uiStatus, uiStatus = uiStatus,
title = resourceReference(R.string.yield_module_transaction_initialize), title = resourceReference(R.string.yield_module_transaction_initialize),
iconRes = R.drawable.ic_gear_24, icon = TxIcon.Res(R.drawable.ic_gear_24),
subtitle = tx.yieldSupplySubtitle(currency, type), subtitle = tx.yieldSupplySubtitle(currency, type),
) )
@ -190,7 +197,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
tx = tx, tx = tx,
uiStatus = uiStatus, uiStatus = uiStatus,
title = resourceReference(R.string.yield_module_transaction_reactivate), title = resourceReference(R.string.yield_module_transaction_reactivate),
iconRes = R.drawable.ic_refresh_24, icon = TxIcon.Vector(Icons.ic_arrow_refresh_20),
subtitle = tx.yieldSupplySubtitle(currency, type), subtitle = tx.yieldSupplySubtitle(currency, type),
) )
@ -206,7 +213,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
} else { } else {
resourceReference(R.string.common_transfer) resourceReference(R.string.common_transfer)
}, },
iconRes = tx.directionalIcon(), icon = tx.directionalIcon(),
subtitle = tx.yieldSupplySubtitle(currency, type), subtitle = tx.yieldSupplySubtitle(currency, type),
hideAmount = currency is CryptoCurrency.Token && !tx.isOutgoing, hideAmount = currency is CryptoCurrency.Token && !tx.isOutgoing,
) )
@ -218,7 +225,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
tx = tx, tx = tx,
uiStatus = uiStatus, uiStatus = uiStatus,
title = resourceReference(R.string.transaction_history_operation), title = resourceReference(R.string.transaction_history_operation),
iconRes = tx.directionalIcon(), icon = tx.directionalIcon(),
subtitle = tx.extractAddressSubtitle(), subtitle = tx.extractAddressSubtitle(),
) )
@ -227,7 +234,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
tx = tx, tx = tx,
uiStatus = uiStatus, uiStatus = uiStatus,
title = resourceReference(R.string.gasless_transaction_fee), title = resourceReference(R.string.gasless_transaction_fee),
iconRes = tx.directionalIcon(), icon = tx.directionalIcon(),
subtitle = tx.extractAddressSubtitle(), subtitle = tx.extractAddressSubtitle(),
) )
@ -235,7 +242,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
tx: TxInfo, tx: TxInfo,
uiStatus: TransactionItemUM.Content.Status, uiStatus: TransactionItemUM.Content.Status,
title: TextReference, title: TextReference,
iconRes: Int, icon: TxIcon,
subtitle: ContentSubtitle, subtitle: ContentSubtitle,
hideAmount: Boolean = false, hideAmount: Boolean = false,
): TransactionItemUM.Content = TransactionItemUM.Content( ): TransactionItemUM.Content = TransactionItemUM.Content(
@ -245,7 +252,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
time = tx.timestampInMillis.toTimeFormat(), time = tx.timestampInMillis.toTimeFormat(),
status = uiStatus, status = uiStatus,
direction = tx.extractDirection(), direction = tx.extractDirection(),
iconRes = if (uiStatus is TransactionItemUM.Content.Status.Failed) R.drawable.ic_close_24 else iconRes, icon = if (uiStatus is TransactionItemUM.Content.Status.Failed) TxIcon.Vector(Icons.ic_cross_20) else icon,
title = title, title = title,
subtitle = subtitle, subtitle = subtitle,
timestamp = tx.timestampInMillis, timestamp = tx.timestampInMillis,
@ -379,20 +386,10 @@ private fun TxInfo.directionalAddressRes(): Int = if (isOutgoing) {
// endregion // endregion
// region Labels
private fun TxInfo.statusAwareTitle(@StringRes pending: Int, @StringRes confirmed: Int): TextReference = when (status) {
is TxInfo.TransactionStatus.Failed ->
resourceReference(R.string.common_action_failed, wrappedList(resourceReference(pending)))
is TxInfo.TransactionStatus.Unconfirmed -> resourceReference(pending)
is TxInfo.TransactionStatus.Confirmed -> resourceReference(confirmed)
}
// endregion
// region Misc // region Misc
private fun TxInfo.directionalIcon(): Int = if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24 private fun TxInfo.directionalIcon(): TxIcon =
TxIcon.Vector(if (isOutgoing) Icons.ic_arrow_up_20 else Icons.ic_arrow_down_20)
private fun TxInfo.extractDirection(): TransactionItemUM.Content.Direction = if (isOutgoing) { private fun TxInfo.extractDirection(): TransactionItemUM.Content.Direction = if (isOutgoing) {
TransactionItemUM.Content.Direction.OUTGOING TransactionItemUM.Content.Direction.OUTGOING
@ -400,10 +397,4 @@ private fun TxInfo.extractDirection(): TransactionItemUM.Content.Direction = if
TransactionItemUM.Content.Direction.INCOMING TransactionItemUM.Content.Direction.INCOMING
} }
private fun TxInfo.TransactionStatus.toUiStatus(): TransactionItemUM.Content.Status = when (this) {
TxInfo.TransactionStatus.Confirmed -> TransactionItemUM.Content.Status.Confirmed
TxInfo.TransactionStatus.Failed -> TransactionItemUM.Content.Status.Failed
TxInfo.TransactionStatus.Unconfirmed -> TransactionItemUM.Content.Status.Unconfirmed
}
// endregion // endregion

View file

@ -3,9 +3,11 @@ package com.tangem.features.txhistory.entity
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.transactions.state.TransactionItemUM import com.tangem.core.ui.components.transactions.state.TransactionItemUM
import com.tangem.core.ui.components.transactions.state.TxIcon
import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.ds.image.DeviceIconUM
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -208,11 +210,26 @@ internal sealed interface TxHistoryDetailsUM : TangemBottomSheetConfigContent {
/** /**
* Shared bottom-sheet top bar. The icon glyph and [title] text come from the transaction type; [status] drives * Shared bottom-sheet top bar. The icon glyph and [title] text come from the transaction type; [status] drives
* the three visual states (in-progress / confirmed / failed) recoloring the icon circle and the title. * the three visual states (in-progress / confirmed / failed) recoloring the icon circle and the title.
*
* [menu] is the header's overflow context-menu content; empty leaves the trailing menu button inert.
*/ */
data class HeaderUM( data class HeaderUM(
@DrawableRes val iconRes: Int, val icon: TxIcon,
val status: TransactionItemUM.Content.Status, val status: TransactionItemUM.Content.Status,
val title: TextReference, val title: TextReference,
val subtitle: TextReference, val subtitle: TextReference,
val menu: ImmutableList<MenuItemUM> = persistentListOf(),
)
/**
* One row of the header's overflow context menu: a leading [icon] glyph and a [title] label. [isDestructive]
* renders the row in the error color (e.g. a remove action); [onClick] runs the action and is expected to also
* dismiss the menu at the call site.
*/
data class MenuItemUM(
val icon: ImageVector,
val title: TextReference,
val isDestructive: Boolean = false,
val onClick: () -> Unit,
) )
} }

View file

@ -4,12 +4,17 @@ import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.domain.txhistory.model.explorerHash
import com.tangem.domain.txhistory.model.idToCopy
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.features.txhistory.component.TxHistoryDetailsComponent import com.tangem.features.txhistory.component.TxHistoryDetailsComponent
import com.tangem.features.txhistory.converter.TxHistoryInfoToTxHistoryDetailsUMConverter import com.tangem.features.txhistory.converter.TxHistoryInfoToTxHistoryDetailsUMConverter
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM import com.tangem.features.txhistory.entity.TxHistoryDetailsUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -19,10 +24,13 @@ import javax.inject.Inject
@Stable @Stable
@ModelScoped @ModelScoped
@Suppress("LongParameterList")
internal class TxHistoryDetailsModel @Inject constructor( internal class TxHistoryDetailsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val clipboardManager: ClipboardManager, private val clipboardManager: ClipboardManager,
private val urlOpener: UrlOpener, private val urlOpener: UrlOpener,
private val shareManager: ShareManager,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
ownerLookupProducer: TxHistoryOwnerLookupProducer, ownerLookupProducer: TxHistoryOwnerLookupProducer,
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
) : Model() { ) : Model() {
@ -33,10 +41,17 @@ internal class TxHistoryDetailsModel @Inject constructor(
params.txHistoryInfo, params.txHistoryInfo,
ownerLookupProducer(), ownerLookupProducer(),
) { txInfo, lookup -> ) { txInfo, lookup ->
// No explorer hash (e.g. an express op with no on-chain leg yet, or a blank on-chain hash) → the "Share" and
// "Explore" rows are dropped; a blank id drops the "Transaction ID" row.
val explorerHash = txInfo.explorerHash?.ifBlank { null }
val idToCopy = txInfo.idToCopy.ifBlank { null }
TxHistoryInfoToTxHistoryDetailsUMConverter( TxHistoryInfoToTxHistoryDetailsUMConverter(
currency = params.currency, currency = params.currency,
onCopyAddress = ::onCopyAddress, onCopyAddress = ::onCopyAddress,
onGoToProvider = urlOpener::openUrl, onGoToProvider = urlOpener::openUrl,
onCopyTxId = idToCopy?.let { id -> { onCopyTxId(id) } },
onShare = explorerHash?.let { hash -> { share(hash) } },
onExplore = explorerHash?.let { hash -> { explore(hash) } },
lookup = lookup, lookup = lookup,
).convert(txInfo) ).convert(txInfo)
} }
@ -47,4 +62,25 @@ internal class TxHistoryDetailsModel @Inject constructor(
private fun onCopyAddress(address: String) { private fun onCopyAddress(address: String) {
clipboardManager.setText(text = address, isSensitive = false) clipboardManager.setText(text = address, isSensitive = false)
} }
/** Copies the transaction id to the clipboard — wired into the header menu's "Transaction ID" row. */
private fun onCopyTxId(id: String) {
clipboardManager.setText(text = id, isSensitive = false)
}
/** Opens the transaction in the blockchain explorer — wired into the header menu's "Explore" row. */
private fun explore(txHash: String) {
getExplorerTransactionUrlUseCase(txHash = txHash, currency = params.currency).fold(
ifLeft = { TangemLogger.e(it.toString()) },
ifRight = { urlOpener.openUrl(url = it) },
)
}
/** Shares the transaction's explorer URL — wired into the header menu's "Share" row. */
private fun share(txHash: String) {
getExplorerTransactionUrlUseCase(txHash = txHash, currency = params.currency).fold(
ifLeft = { TangemLogger.e(it.toString()) },
ifRight = { shareManager.shareText(text = it) },
)
}
} }

View file

@ -10,9 +10,15 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status
import com.tangem.core.ui.components.transactions.state.TxIcon
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_swap_horizontal_20
import com.tangem.core.ui.res.generated.icons.ic_arrow_up_20
import com.tangem.core.ui.res.generated.icons.ic_copy_24
import com.tangem.core.ui.res.generated.icons.ic_globe_24
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM import com.tangem.features.txhistory.entity.TxHistoryDetailsUM
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -65,10 +71,11 @@ private fun TxHistoryDetailsModalBottomSheetContentTwoAssetsPreview() {
/** Fully-populated single-asset state exercising every sub-view: header, amount block, counterparty and info rows. */ /** Fully-populated single-asset state exercising every sub-view: header, amount block, counterparty and info rows. */
private fun previewSingleAsset() = TxHistoryDetailsUM.SingleAsset( private fun previewSingleAsset() = TxHistoryDetailsUM.SingleAsset(
header = TxHistoryDetailsUM.HeaderUM( header = TxHistoryDetailsUM.HeaderUM(
iconRes = R.drawable.ic_arrow_up_24, icon = TxIcon.Vector(Icons.ic_arrow_up_20),
status = Status.Confirmed, status = Status.Confirmed,
title = stringReference("Sent"), title = stringReference("Sent"),
subtitle = stringReference("Jan 20 2026, 9:24 PM"), subtitle = stringReference("Jan 20 2026, 9:24 PM"),
menu = previewMenu(),
), ),
amountBlock = TxHistoryDetailsUM.AmountBlockUM( amountBlock = TxHistoryDetailsUM.AmountBlockUM(
currencyIcon = CurrencyIconState.CoinIcon( currencyIcon = CurrencyIconState.CoinIcon(
@ -97,10 +104,11 @@ private fun previewSingleAsset() = TxHistoryDetailsUM.SingleAsset(
/** Failed swap exercising the two-asset body: both legs, the error status banner, provider link row and the CTA. */ /** Failed swap exercising the two-asset body: both legs, the error status banner, provider link row and the CTA. */
private fun previewTwoAssets() = TxHistoryDetailsUM.TwoAssets( private fun previewTwoAssets() = TxHistoryDetailsUM.TwoAssets(
header = TxHistoryDetailsUM.HeaderUM( header = TxHistoryDetailsUM.HeaderUM(
iconRes = R.drawable.ic_exchange_vertical_24, icon = TxIcon.Vector(Icons.ic_arrow_swap_horizontal_20),
status = Status.Failed, status = Status.Failed,
title = stringReference("Swap"), title = stringReference("Swap"),
subtitle = stringReference("Jan 20 2026, 9:24 PM"), subtitle = stringReference("Jan 20 2026, 9:24 PM"),
menu = previewMenu(),
), ),
from = TxHistoryDetailsUM.AssetUM( from = TxHistoryDetailsUM.AssetUM(
label = stringReference("You send"), label = stringReference("You send"),
@ -147,4 +155,18 @@ private fun previewTwoAssets() = TxHistoryDetailsUM.TwoAssets(
), ),
) )
/** Sample header `•••` menu used by the previews. */
private fun previewMenu() = persistentListOf(
TxHistoryDetailsUM.MenuItemUM(
icon = Icons.ic_copy_24,
title = stringReference("Transaction ID"),
onClick = {},
),
TxHistoryDetailsUM.MenuItemUM(
icon = Icons.ic_globe_24,
title = stringReference("Explore"),
onClick = {},
),
)
// endregion // endregion

View file

@ -2,26 +2,37 @@ package com.tangem.features.txhistory.ui
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status
import com.tangem.core.ui.components.transactions.state.TxIcon
import com.tangem.core.ui.components.transactions.state.asImageVector
import com.tangem.core.ui.ds.contextmenu.TangemContextMenu
import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds2.button.Close import com.tangem.core.ui.ds2.button.Close
import com.tangem.core.ui.ds2.button.TangemButton import com.tangem.core.ui.ds2.button.TangemButton
@ -33,15 +44,20 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_swap_horizontal_20
import com.tangem.core.ui.res.generated.icons.ic_copy_24
import com.tangem.core.ui.res.generated.icons.ic_dots_horizontal_20 import com.tangem.core.ui.res.generated.icons.ic_dots_horizontal_20
import com.tangem.core.ui.res.generated.icons.ic_info_24
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM import com.tangem.features.txhistory.entity.TxHistoryDetailsUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/** /**
* Shared top navigation ("Nav bar") for the transaction details bottom sheet, common to all transaction types. * Shared top navigation ("Nav bar") for the transaction details bottom sheet, common to all transaction types.
* *
* Built on the redesigned [TangemTopNavigation]: a leading status-tinted action icon ([StatusActionIcon]) in the start * Built on the redesigned [TangemTopNavigation]: a leading status-tinted action icon ([StatusActionIcon]) in the start
* slot, a status-colored [title][TxHistoryDetailsUM.HeaderUM.title] over a date subtitle in the center slot, and the * slot, a status-colored [title][TxHistoryDetailsUM.HeaderUM.title] over a date subtitle in the center slot, and the
* trailing context-menu (``, grouped in a Material pill) + close (``) buttons in the end slots. * trailing overflow context-menu (grouped in a Material pill) + close buttons in the end slots.
* *
* Three visual states are driven by [TxHistoryDetailsUM.HeaderUM.status]: the action-icon circle background, the icon * Three visual states are driven by [TxHistoryDetailsUM.HeaderUM.status]: the action-icon circle background, the icon
* tint and the title color change between in-progress (brand/blue), confirmed (neutral) and failed (red). The icon * tint and the title color change between in-progress (brand/blue), confirmed (neutral) and failed (red). The icon
@ -59,16 +75,13 @@ internal fun TxHistoryDetailsTopNavigation(
TangemTopNavigation( TangemTopNavigation(
modifier = modifier, modifier = modifier,
windowInsets = WindowInsets(0), windowInsets = WindowInsets(0),
contentPadding = PaddingValues(16.dp),
blurBackground = false, blurBackground = false,
startButton = { StatusActionIcon(iconRes = header.iconRes, status = header.status) }, startButton = { StatusActionIcon(icon = header.icon, status = header.status) },
endButtonsGroup = { endButtonsGroup = {
// Context menu. Click handling is intentionally not wired yet. if (header.menu.isNotEmpty()) {
TangemButton( TxHistoryDetailsOverflowMenu(menu = header.menu)
variant = TangemButton.Variant.Ghost, }
iconStart = TangemIconUM.Icon(Icons.ic_dots_horizontal_20),
contentDescription = resourceReference(R.string.common_more).resolveReference(),
onClick = {},
)
}, },
endButton = { TangemButton.Close(onClick = onCloseClick) }, endButton = { TangemButton.Close(onClick = onCloseClick) },
contentColumn = { contentColumn = {
@ -88,8 +101,93 @@ internal fun TxHistoryDetailsTopNavigation(
) )
} }
/**
* The header's "•••" overflow button and its drop-down [TangemContextMenu]. A destructive row (e.g. "Hide transaction")
* is preceded by a divider when it is not the first item. Tapping a row closes the menu and fires the item's action.
*/
@Composable @Composable
private fun StatusActionIcon(iconRes: Int, status: Status, modifier: Modifier = Modifier) { private fun TxHistoryDetailsOverflowMenu(
menu: ImmutableList<TxHistoryDetailsUM.MenuItemUM>,
modifier: Modifier = Modifier,
) {
var isMenuExpanded by remember { mutableStateOf(false) }
Box(modifier = modifier) {
TangemButton(
variant = TangemButton.Variant.Ghost,
iconStart = TangemIconUM.Icon(Icons.ic_dots_horizontal_20),
contentDescription = resourceReference(R.string.common_more).resolveReference(),
onClick = { isMenuExpanded = true },
)
TangemContextMenu(
expanded = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false },
) {
menu.forEachIndexed { index, item ->
if (item.isDestructive && index > 0) {
HorizontalDivider(
thickness = 0.5.dp,
color = TangemTheme.colors2.border.neutral.quaternary,
)
}
TxHistoryDetailsMenuItem(
item = item,
onClick = {
isMenuExpanded = false
item.onClick()
},
)
}
}
}
}
/**
* One row of the header's overflow context menu: a leading 24dp glyph and a label, padded to the iOS-style menu metrics
* (20dp horizontal, 10dp vertical, 8dp gap). A [destructive][TxHistoryDetailsUM.MenuItemUM.isDestructive] row is tinted
* with the error color (icon + text).
*/
@Composable
private fun TxHistoryDetailsMenuItem(
item: TxHistoryDetailsUM.MenuItemUM,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val textColor = if (item.isDestructive) {
TangemTheme.colors3.text.status.error
} else {
TangemTheme.colors3.text.primary
}
val iconTint = if (item.isDestructive) {
TangemTheme.colors3.icon.status.error
} else {
TangemTheme.colors3.icon.primary
}
Row(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 20.dp, vertical = 10.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = item.icon,
contentDescription = null,
tint = iconTint,
modifier = Modifier.size(24.dp),
)
Text(
text = item.title.resolveReference(),
color = textColor,
style = TangemTheme.typography3.body.medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
@Composable
private fun StatusActionIcon(icon: TxIcon, status: Status, modifier: Modifier = Modifier) {
Box( Box(
modifier = modifier modifier = modifier
.size(44.dp) .size(44.dp)
@ -98,7 +196,7 @@ private fun StatusActionIcon(iconRes: Int, status: Status, modifier: Modifier =
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Icon( Icon(
painter = painterResource(iconRes), imageVector = icon.asImageVector(),
contentDescription = null, contentDescription = null,
tint = status.iconTint, tint = status.iconTint,
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
@ -161,10 +259,24 @@ private fun TxHistoryDetailsTopNavigationPreview() {
} }
private fun previewHeader(status: Status, title: TextReference) = TxHistoryDetailsUM.HeaderUM( private fun previewHeader(status: Status, title: TextReference) = TxHistoryDetailsUM.HeaderUM(
iconRes = R.drawable.ic_exchange_vertical_24, icon = TxIcon.Vector(Icons.ic_arrow_swap_horizontal_20),
status = status, status = status,
title = title, title = title,
subtitle = stringReference("Jan 20 2026, 9:24 PM"), subtitle = stringReference("Jan 20 2026, 9:24 PM"),
menu = previewMenu(),
)
private fun previewMenu() = persistentListOf(
TxHistoryDetailsUM.MenuItemUM(
icon = Icons.ic_copy_24,
title = stringReference("Transaction ID"),
onClick = {},
),
TxHistoryDetailsUM.MenuItemUM(
icon = Icons.ic_info_24,
title = stringReference("Explore"),
onClick = {},
),
) )
// endregion // endregion

View file

@ -5,10 +5,18 @@ import androidx.compose.ui.graphics.Color
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.core.ui.components.transactions.state.TransactionItemUM import com.tangem.core.ui.components.transactions.state.TransactionItemUM
import com.tangem.core.ui.components.transactions.state.TxIcon
import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.ds.image.DeviceIconUM
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_down_20
import com.tangem.core.ui.res.generated.icons.ic_arrow_swap_horizontal_20
import com.tangem.core.ui.res.generated.icons.ic_card_20
import com.tangem.core.ui.res.generated.icons.ic_copy_24
import com.tangem.core.ui.res.generated.icons.ic_globe_24
import com.tangem.core.ui.res.generated.icons.ic_share_android_24
import com.tangem.domain.express.models.ExchangeTransaction import com.tangem.domain.express.models.ExchangeTransaction
import com.tangem.domain.express.models.ExpressAsset.ID as ExpressAssetId import com.tangem.domain.express.models.ExpressAsset.ID as ExpressAssetId
import com.tangem.domain.express.models.ExpressExchangeStatus import com.tangem.domain.express.models.ExpressExchangeStatus
@ -128,7 +136,7 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
val header = converter.convert(tx).header val header = converter.convert(tx).header
// Assert // Assert
assertThat(header.iconRes).isEqualTo(R.drawable.ic_arrow_down_24) assertThat(header.icon).isEqualTo(TxIcon.Vector(Icons.ic_arrow_down_20))
assertThat(header.status).isEqualTo(TransactionItemUM.Content.Status.Confirmed) assertThat(header.status).isEqualTo(TransactionItemUM.Content.Status.Confirmed)
assertThat(header.title).isEqualTo(resourceReference(R.string.common_received)) assertThat(header.title).isEqualTo(resourceReference(R.string.common_received))
} }
@ -202,7 +210,81 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
val header = converter.convert(tx).header val header = converter.convert(tx).header
// Assert // Assert
assertThat(header.iconRes).isEqualTo(R.drawable.ic_exchange_vertical_24) assertThat(header.icon).isEqualTo(TxIcon.Vector(Icons.ic_arrow_swap_horizontal_20))
}
@Test
fun `GIVEN menu callbacks WHEN convert THEN header menu has copy-id, share and explore rows wired`() {
// Arrange
var copiedTxId = false
var shared = false
var explored = false
val menuConverter = TxHistoryInfoToTxHistoryDetailsUMConverter(
currency = currency,
onCopyAddress = copiedAddresses::add,
onGoToProvider = openedUrls::add,
onCopyTxId = { copiedTxId = true },
onShare = { shared = true },
onExplore = { explored = true },
)
// Act
val menu = menuConverter.convert(onChain(type = TransactionType.Transfer)).header.menu
// Assert
assertThat(menu).hasSize(3)
assertThat(menu[0].icon).isEqualTo(Icons.ic_copy_24)
assertThat(menu[0].title).isEqualTo(resourceReference(R.string.common_transaction_id))
assertThat(menu[1].icon).isEqualTo(Icons.ic_share_android_24)
assertThat(menu[1].title).isEqualTo(resourceReference(R.string.common_share))
assertThat(menu[2].icon).isEqualTo(Icons.ic_globe_24)
assertThat(menu[2].title).isEqualTo(resourceReference(R.string.common_explore))
menu[0].onClick()
menu[1].onClick()
menu[2].onClick()
assertThat(copiedTxId).isTrue()
assertThat(shared).isTrue()
assertThat(explored).isTrue()
}
@Test
fun `GIVEN no share and explore callbacks WHEN convert THEN header menu drops the share and explore rows`() {
// Arrange — onShare/onExplore are null (e.g. an express op with no on-chain leg to share or open yet).
val menuConverter = TxHistoryInfoToTxHistoryDetailsUMConverter(
currency = currency,
onCopyAddress = copiedAddresses::add,
onGoToProvider = openedUrls::add,
onCopyTxId = {},
onShare = null,
onExplore = null,
)
// Act
val menu = menuConverter.convert(onChain(type = TransactionType.Transfer)).header.menu
// Assert
assertThat(menu).hasSize(1)
assertThat(menu[0].title).isEqualTo(resourceReference(R.string.common_transaction_id))
}
@Test
fun `GIVEN no menu callbacks WHEN convert THEN header menu is empty`() {
// Arrange — every menu action is absent (e.g. a blank tx id with no on-chain leg to share or open).
val menuConverter = TxHistoryInfoToTxHistoryDetailsUMConverter(
currency = currency,
onCopyAddress = copiedAddresses::add,
onGoToProvider = openedUrls::add,
onCopyTxId = null,
onShare = null,
onExplore = null,
)
// Act
val menu = menuConverter.convert(onChain(type = TransactionType.Transfer)).header.menu
// Assert
assertThat(menu).isEmpty()
} }
@Test @Test
@ -364,7 +446,7 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
// Assert // Assert
assertThat(result).isInstanceOf(TxHistoryDetailsUM.TwoAssets::class.java) assertThat(result).isInstanceOf(TxHistoryDetailsUM.TwoAssets::class.java)
assertThat(result.header.iconRes).isEqualTo(R.drawable.ic_exchange_vertical_24) assertThat(result.header.icon).isEqualTo(TxIcon.Vector(Icons.ic_arrow_swap_horizontal_20))
} }
@Test @Test
@ -649,7 +731,7 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
val result = converter.convert(expressOnramp(status = ExpressOnrampStatus.Finished)) as TxHistoryDetailsUM.TwoAssets val result = converter.convert(expressOnramp(status = ExpressOnrampStatus.Finished)) as TxHistoryDetailsUM.TwoAssets
// Assert // Assert
assertThat(result.header.iconRes).isEqualTo(R.drawable.ic_tangem_card_24) assertThat(result.header.icon).isEqualTo(TxIcon.Vector(Icons.ic_card_20))
assertThat(result.statusBanner).isEqualTo( assertThat(result.statusBanner).isEqualTo(
TxHistoryDetailsUM.StatusBannerUM( TxHistoryDetailsUM.StatusBannerUM(
severity = TxHistoryDetailsUM.StatusBannerUM.Severity.Success, severity = TxHistoryDetailsUM.StatusBannerUM.Severity.Success,

View file

@ -3,8 +3,15 @@ package com.tangem.features.txhistory.converter
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import com.tangem.core.ui.components.transactions.state.TransactionItemUM import com.tangem.core.ui.components.transactions.state.TransactionItemUM
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.ContentSubtitle import com.tangem.core.ui.components.transactions.state.TransactionItemUM.ContentSubtitle
import com.tangem.core.ui.components.transactions.state.TxIcon
import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.ds.image.DeviceIconUM
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_down_20
import com.tangem.core.ui.res.generated.icons.ic_arrow_refresh_20
import com.tangem.core.ui.res.generated.icons.ic_arrow_up_20
import com.tangem.core.ui.res.generated.icons.ic_cross_20
import com.tangem.core.ui.res.generated.icons.ic_document_20
import com.tangem.domain.models.account.Account.CryptoPortfolio.Companion.createMainAccount import com.tangem.domain.models.account.Account.CryptoPortfolio.Companion.createMainAccount
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.Network
@ -78,7 +85,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
val result = coinConverter.convert(tx) as TransactionItemUM.Content val result = coinConverter.convert(tx) as TransactionItemUM.Content
assertThat(result.title).isEqualTo(TextReference.Str("Mint NFT")) assertThat(result.title).isEqualTo(TextReference.Str("Mint NFT"))
assertThat(result.iconRes).isEqualTo(R.drawable.ic_arrow_down_24) assertThat(result.icon).isEqualTo(TxIcon.Vector(Icons.ic_arrow_down_20))
} }
@Test @Test
@ -119,7 +126,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
assertThat(result.title).isEqualTo( assertThat(result.title).isEqualTo(
resRef(R.string.common_action_failed, listOf(resRef(R.string.common_swapping))), resRef(R.string.common_action_failed, listOf(resRef(R.string.common_swapping))),
) )
assertThat(result.iconRes).isEqualTo(R.drawable.ic_close_24) assertThat(result.icon).isEqualTo(TxIcon.Vector(Icons.ic_cross_20))
} }
@Test @Test
@ -193,7 +200,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
assertThat(result.title).isEqualTo(resRef(R.string.common_sent)) assertThat(result.title).isEqualTo(resRef(R.string.common_sent))
assertThat(result.direction).isEqualTo(TransactionItemUM.Content.Direction.OUTGOING) assertThat(result.direction).isEqualTo(TransactionItemUM.Content.Direction.OUTGOING)
assertThat(result.iconRes).isEqualTo(R.drawable.ic_arrow_up_24) assertThat(result.icon).isEqualTo(TxIcon.Vector(Icons.ic_arrow_up_20))
val subtitle = result.subtitle as ContentSubtitle.ExternalAddress val subtitle = result.subtitle as ContentSubtitle.ExternalAddress
assertThat(subtitle.direction).isEqualTo(ContentSubtitle.Direction.TO) assertThat(subtitle.direction).isEqualTo(ContentSubtitle.Direction.TO)
assertThat(subtitle.rawAddress).isEqualTo(USER_ADDRESS) assertThat(subtitle.rawAddress).isEqualTo(USER_ADDRESS)
@ -242,7 +249,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
assertThat(result.title).isEqualTo(resRef(R.string.common_received)) assertThat(result.title).isEqualTo(resRef(R.string.common_received))
assertThat(result.direction).isEqualTo(TransactionItemUM.Content.Direction.INCOMING) assertThat(result.direction).isEqualTo(TransactionItemUM.Content.Direction.INCOMING)
assertThat(result.iconRes).isEqualTo(R.drawable.ic_arrow_down_24) assertThat(result.icon).isEqualTo(TxIcon.Vector(Icons.ic_arrow_down_20))
val subtitle = result.subtitle as ContentSubtitle.ExternalAddress val subtitle = result.subtitle as ContentSubtitle.ExternalAddress
assertThat(subtitle.direction).isEqualTo(ContentSubtitle.Direction.FROM) assertThat(subtitle.direction).isEqualTo(ContentSubtitle.Direction.FROM)
} }
@ -436,7 +443,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
val result = coinConverter.convert(tx) as TransactionItemUM.Content val result = coinConverter.convert(tx) as TransactionItemUM.Content
assertThat(result.title).isEqualTo(resRef(R.string.yield_module_transaction_deploy_contract)) assertThat(result.title).isEqualTo(resRef(R.string.yield_module_transaction_deploy_contract))
assertThat(result.iconRes).isEqualTo(R.drawable.ic_doc_24) assertThat(result.icon).isEqualTo(TxIcon.Vector(Icons.ic_document_20))
} }
@Test @Test
@ -446,7 +453,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
val result = coinConverter.convert(tx) as TransactionItemUM.Content val result = coinConverter.convert(tx) as TransactionItemUM.Content
assertThat(result.title).isEqualTo(resRef(R.string.yield_module_transaction_initialize)) assertThat(result.title).isEqualTo(resRef(R.string.yield_module_transaction_initialize))
assertThat(result.iconRes).isEqualTo(R.drawable.ic_gear_24) assertThat(result.icon).isEqualTo(TxIcon.Res(R.drawable.ic_gear_24))
} }
@Test @Test
@ -456,7 +463,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
val result = coinConverter.convert(tx) as TransactionItemUM.Content val result = coinConverter.convert(tx) as TransactionItemUM.Content
assertThat(result.title).isEqualTo(resRef(R.string.yield_module_transaction_reactivate)) assertThat(result.title).isEqualTo(resRef(R.string.yield_module_transaction_reactivate))
assertThat(result.iconRes).isEqualTo(R.drawable.ic_refresh_24) assertThat(result.icon).isEqualTo(TxIcon.Vector(Icons.ic_arrow_refresh_20))
} }
@Test @Test
@ -655,7 +662,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
val result = coinConverter.convert(tx) as TransactionItemUM.Content val result = coinConverter.convert(tx) as TransactionItemUM.Content
assertThat(result.iconRes).isEqualTo(R.drawable.ic_close_24) assertThat(result.icon).isEqualTo(TxIcon.Vector(Icons.ic_cross_20))
} }
@Test @Test