Updated on 2026-08-14
This commit is contained in:
parent
51e650c360
commit
391e0562cc
13 changed files with 519 additions and 115 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue