Updated on 2026-08-14
This commit is contained in:
parent
51e650c360
commit
391e0562cc
13 changed files with 519 additions and 115 deletions
|
|
@ -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.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.TxIcon
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
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.domain.express.models.ExpressExchangeStatus
|
||||
import com.tangem.domain.express.models.ExpressOnrampStatus
|
||||
|
|
@ -63,11 +67,11 @@ internal class ExpressTxToTransactionItemUMConverter(
|
|||
status = status,
|
||||
amount = formatAmount(viewedAmount, prefix),
|
||||
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),
|
||||
subtitle = ContentSubtitle.Asset(
|
||||
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),
|
||||
),
|
||||
warning = swapWarning(swap),
|
||||
|
|
@ -86,7 +90,7 @@ internal class ExpressTxToTransactionItemUMConverter(
|
|||
status = status,
|
||||
amount = formatAmount(onramp.tx.toAsset.amount, prefix),
|
||||
direction = RowDirection.INCOMING,
|
||||
iconRes = R.drawable.ic_tangem_card_24,
|
||||
icon = TxIcon.Vector(Icons.ic_card_20),
|
||||
title = onrampTitle(status),
|
||||
subtitle = ContentSubtitle.Asset(
|
||||
direction = SubtitleDirection.FROM,
|
||||
|
|
@ -106,7 +110,7 @@ internal class ExpressTxToTransactionItemUMConverter(
|
|||
status: Status,
|
||||
amount: String?,
|
||||
direction: RowDirection,
|
||||
iconRes: Int,
|
||||
icon: TxIcon,
|
||||
title: TextReference,
|
||||
subtitle: ContentSubtitle,
|
||||
warning: TextReference?,
|
||||
|
|
@ -120,7 +124,7 @@ internal class ExpressTxToTransactionItemUMConverter(
|
|||
status = status,
|
||||
direction = direction,
|
||||
onClick = { txHistoryUiActions.onTransactionClick(tx) },
|
||||
iconRes = iconRes,
|
||||
icon = icon,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
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.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
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.resourceReference
|
||||
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.fiat
|
||||
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.domain.express.models.ExchangeTransaction
|
||||
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.TransactionType
|
||||
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.OnChainTx
|
||||
import com.tangem.domain.txhistory.model.TxHistoryInfo
|
||||
|
|
@ -57,6 +64,9 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverter(
|
|||
private val currency: CryptoCurrency,
|
||||
private val onCopyAddress: (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(
|
||||
ownAccountByNetwork = emptyMap(),
|
||||
isAccountsModeEnabled = false,
|
||||
|
|
@ -94,12 +104,49 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverter(
|
|||
)
|
||||
|
||||
private fun TxInfo.toHeaderUM(): TxHistoryDetailsUM.HeaderUM = TxHistoryDetailsUM.HeaderUM(
|
||||
iconRes = headerIcon(),
|
||||
icon = headerIcon(),
|
||||
status = status.toUiStatus(),
|
||||
title = headerTitle(),
|
||||
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(
|
||||
currencyIcon = iconStateConverter.convert(currency),
|
||||
amount = stringReference(signedAmount(currency)),
|
||||
|
|
@ -163,10 +210,11 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverter(
|
|||
val toOwner = resolveLegOwner(swap.tx.payoutAddress, swap.tx.toAsset.cryptoCurrency)
|
||||
return TxHistoryDetailsUM.TwoAssets(
|
||||
header = TxHistoryDetailsUM.HeaderUM(
|
||||
iconRes = R.drawable.ic_exchange_vertical_24,
|
||||
icon = TxIcon.Vector(Icons.ic_arrow_swap_horizontal_20),
|
||||
status = status,
|
||||
title = status.statusAwareTitle(R.string.common_swapping, R.string.common_swapped),
|
||||
subtitle = headerSubtitle(swap.timestampMillis),
|
||||
menu = buildMenu(),
|
||||
),
|
||||
from = swap.tx.fromAsset.toAssetUM(
|
||||
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)
|
||||
return TxHistoryDetailsUM.TwoAssets(
|
||||
header = TxHistoryDetailsUM.HeaderUM(
|
||||
iconRes = R.drawable.ic_tangem_card_24,
|
||||
icon = TxIcon.Vector(Icons.ic_card_20),
|
||||
status = status,
|
||||
title = status.statusAwareTitle(
|
||||
R.string.tx_history_onramp_top_up,
|
||||
R.string.tx_history_onramp_topped_up,
|
||||
),
|
||||
subtitle = headerSubtitle(onramp.timestampMillis),
|
||||
menu = buildMenu(),
|
||||
),
|
||||
from = onramp.tx.fromFiat.toFiatAssetUM(
|
||||
// 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,
|
||||
isFaded: Boolean,
|
||||
): TxHistoryDetailsUM.AssetUM {
|
||||
val symbol = cryptoCurrency?.symbol ?: id.networkId
|
||||
val symbol = displaySymbol
|
||||
val formatted = amount.format { crypto(
|
||||
symbol = symbol,
|
||||
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.
|
||||
*/
|
||||
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)
|
||||
.format { fiat(fiatCurrencyCode = code, fiatCurrencySymbol = currencySymbol) }
|
||||
return TxHistoryDetailsUM.AssetUM(
|
||||
|
|
@ -421,12 +470,6 @@ private fun verificationBanner() = TxHistoryDetailsUM.StatusBannerUM(
|
|||
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
|
||||
|
||||
// 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 toAmount = toAsset.amount.takeIfPositive() ?: return null
|
||||
val rate = toAmount.divide(fromAmount, rateScale(toAsset.decimals), RoundingMode.HALF_UP)
|
||||
val baseSymbol = fromAsset.cryptoCurrency?.symbol ?: fromAsset.id.networkId
|
||||
val quoteSymbol = toAsset.cryptoCurrency?.symbol ?: toAsset.id.networkId
|
||||
val baseSymbol = fromAsset.displaySymbol
|
||||
val quoteSymbol = toAsset.displaySymbol
|
||||
val value = rateText(
|
||||
base = oneOf(baseSymbol),
|
||||
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
|
||||
// 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 cryptoSymbol = toAsset.cryptoCurrency?.symbol ?: toAsset.id.networkId
|
||||
val fiatCode = (fromFiat.type as? AmountType.FiatType)?.code ?: fromFiat.currencySymbol
|
||||
val cryptoSymbol = toAsset.displaySymbol
|
||||
val fiatCode = fromFiat.fiatCode
|
||||
val value = rateText(
|
||||
base = oneOf(cryptoSymbol),
|
||||
quote = rate.format { fiat(fiatCurrencyCode = fiatCode, fiatCurrencySymbol = fromFiat.currencySymbol) },
|
||||
|
|
@ -576,9 +619,9 @@ private fun TxInfo.signedAmount(currency: CryptoCurrency): String {
|
|||
// region Header building helpers
|
||||
|
||||
/** Type glyph. Unlike the history list, the failed state keeps the type glyph (only the color changes). */
|
||||
private fun TxInfo.headerIcon(): Int = when (type) {
|
||||
is TransactionType.Swap -> R.drawable.ic_exchange_vertical_24
|
||||
else -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24
|
||||
private fun TxInfo.headerIcon(): TxIcon = when (type) {
|
||||
is TransactionType.Swap -> TxIcon.Vector(Icons.ic_arrow_swap_horizontal_20)
|
||||
else -> TxIcon.Vector(if (isOutgoing) Icons.ic_arrow_up_20 else Icons.ic_arrow_down_20)
|
||||
}
|
||||
|
||||
private fun headerSubtitle(timestampMillis: Long): TextReference {
|
||||
|
|
@ -588,17 +631,4 @@ private fun headerSubtitle(timestampMillis: Long): TextReference {
|
|||
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
|
||||
|
|
@ -6,10 +6,17 @@ import com.tangem.common.ui.account.getUiColor
|
|||
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.ContentSubtitle
|
||||
import com.tangem.core.ui.components.transactions.state.TxIcon
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
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.format
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
|
|
@ -85,7 +92,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
|
|||
tx = tx,
|
||||
uiStatus = uiStatus,
|
||||
title = stringReference(type.name),
|
||||
iconRes = tx.directionalIcon(),
|
||||
icon = tx.directionalIcon(),
|
||||
subtitle = tx.extractAddressSubtitle(),
|
||||
)
|
||||
|
||||
|
|
@ -94,7 +101,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
|
|||
tx = tx,
|
||||
uiStatus = uiStatus,
|
||||
title = tx.statusAwareTitle(R.string.common_swapping, R.string.common_swapped),
|
||||
iconRes = tx.directionalIcon(),
|
||||
icon = tx.directionalIcon(),
|
||||
subtitle = tx.extractAddressSubtitle(),
|
||||
)
|
||||
|
||||
|
|
@ -129,7 +136,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
|
|||
tx = tx,
|
||||
uiStatus = uiStatus,
|
||||
title = title,
|
||||
iconRes = tx.directionalIcon(),
|
||||
icon = tx.directionalIcon(),
|
||||
subtitle = subtitle,
|
||||
)
|
||||
}
|
||||
|
|
@ -142,7 +149,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
|
|||
pending = R.string.transaction_history_claiming_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)),
|
||||
)
|
||||
|
||||
|
|
@ -154,7 +161,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
|
|||
tx = tx,
|
||||
uiStatus = uiStatus,
|
||||
title = resourceReference(R.string.yield_module_transaction_topup),
|
||||
iconRes = tx.directionalIcon(),
|
||||
icon = tx.directionalIcon(),
|
||||
subtitle = tx.yieldSupplySubtitle(currency, type),
|
||||
)
|
||||
|
||||
|
|
@ -166,7 +173,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
|
|||
tx = tx,
|
||||
uiStatus = uiStatus,
|
||||
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),
|
||||
)
|
||||
|
||||
|
|
@ -178,7 +185,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
|
|||
tx = tx,
|
||||
uiStatus = uiStatus,
|
||||
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),
|
||||
)
|
||||
|
||||
|
|
@ -190,7 +197,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
|
|||
tx = tx,
|
||||
uiStatus = uiStatus,
|
||||
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),
|
||||
)
|
||||
|
||||
|
|
@ -206,7 +213,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
|
|||
} else {
|
||||
resourceReference(R.string.common_transfer)
|
||||
},
|
||||
iconRes = tx.directionalIcon(),
|
||||
icon = tx.directionalIcon(),
|
||||
subtitle = tx.yieldSupplySubtitle(currency, type),
|
||||
hideAmount = currency is CryptoCurrency.Token && !tx.isOutgoing,
|
||||
)
|
||||
|
|
@ -218,7 +225,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
|
|||
tx = tx,
|
||||
uiStatus = uiStatus,
|
||||
title = resourceReference(R.string.transaction_history_operation),
|
||||
iconRes = tx.directionalIcon(),
|
||||
icon = tx.directionalIcon(),
|
||||
subtitle = tx.extractAddressSubtitle(),
|
||||
)
|
||||
|
||||
|
|
@ -227,7 +234,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
|
|||
tx = tx,
|
||||
uiStatus = uiStatus,
|
||||
title = resourceReference(R.string.gasless_transaction_fee),
|
||||
iconRes = tx.directionalIcon(),
|
||||
icon = tx.directionalIcon(),
|
||||
subtitle = tx.extractAddressSubtitle(),
|
||||
)
|
||||
|
||||
|
|
@ -235,7 +242,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
|
|||
tx: TxInfo,
|
||||
uiStatus: TransactionItemUM.Content.Status,
|
||||
title: TextReference,
|
||||
iconRes: Int,
|
||||
icon: TxIcon,
|
||||
subtitle: ContentSubtitle,
|
||||
hideAmount: Boolean = false,
|
||||
): TransactionItemUM.Content = TransactionItemUM.Content(
|
||||
|
|
@ -245,7 +252,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
|
|||
time = tx.timestampInMillis.toTimeFormat(),
|
||||
status = uiStatus,
|
||||
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,
|
||||
subtitle = subtitle,
|
||||
timestamp = tx.timestampInMillis,
|
||||
|
|
@ -379,20 +386,10 @@ private fun TxInfo.directionalAddressRes(): Int = if (isOutgoing) {
|
|||
|
||||
// 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
|
||||
|
||||
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) {
|
||||
TransactionItemUM.Content.Direction.OUTGOING
|
||||
|
|
@ -400,10 +397,4 @@ private fun TxInfo.extractDirection(): TransactionItemUM.Content.Direction = if
|
|||
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
|
||||
|
|
@ -3,9 +3,11 @@ package com.tangem.features.txhistory.entity
|
|||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.currency.icon.CurrencyIconState
|
||||
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.extensions.TextReference
|
||||
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
|
||||
* 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(
|
||||
@DrawableRes val iconRes: Int,
|
||||
val icon: TxIcon,
|
||||
val status: TransactionItemUM.Content.Status,
|
||||
val title: 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.model.Model
|
||||
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.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.converter.TxHistoryInfoToTxHistoryDetailsUMConverter
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
|
@ -19,10 +24,13 @@ import javax.inject.Inject
|
|||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class TxHistoryDetailsModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val shareManager: ShareManager,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
ownerLookupProducer: TxHistoryOwnerLookupProducer,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
|
@ -33,10 +41,17 @@ internal class TxHistoryDetailsModel @Inject constructor(
|
|||
params.txHistoryInfo,
|
||||
ownerLookupProducer(),
|
||||
) { 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(
|
||||
currency = params.currency,
|
||||
onCopyAddress = ::onCopyAddress,
|
||||
onGoToProvider = urlOpener::openUrl,
|
||||
onCopyTxId = idToCopy?.let { id -> { onCopyTxId(id) } },
|
||||
onShare = explorerHash?.let { hash -> { share(hash) } },
|
||||
onExplore = explorerHash?.let { hash -> { explore(hash) } },
|
||||
lookup = lookup,
|
||||
).convert(txInfo)
|
||||
}
|
||||
|
|
@ -47,4 +62,25 @@ internal class TxHistoryDetailsModel @Inject constructor(
|
|||
private fun onCopyAddress(address: String) {
|
||||
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.currency.icon.CurrencyIconState
|
||||
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.res.TangemTheme
|
||||
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 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. */
|
||||
private fun previewSingleAsset() = TxHistoryDetailsUM.SingleAsset(
|
||||
header = TxHistoryDetailsUM.HeaderUM(
|
||||
iconRes = R.drawable.ic_arrow_up_24,
|
||||
icon = TxIcon.Vector(Icons.ic_arrow_up_20),
|
||||
status = Status.Confirmed,
|
||||
title = stringReference("Sent"),
|
||||
subtitle = stringReference("Jan 20 2026, 9:24 PM"),
|
||||
menu = previewMenu(),
|
||||
),
|
||||
amountBlock = TxHistoryDetailsUM.AmountBlockUM(
|
||||
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. */
|
||||
private fun previewTwoAssets() = TxHistoryDetailsUM.TwoAssets(
|
||||
header = TxHistoryDetailsUM.HeaderUM(
|
||||
iconRes = R.drawable.ic_exchange_vertical_24,
|
||||
icon = TxIcon.Vector(Icons.ic_arrow_swap_horizontal_20),
|
||||
status = Status.Failed,
|
||||
title = stringReference("Swap"),
|
||||
subtitle = stringReference("Jan 20 2026, 9:24 PM"),
|
||||
menu = previewMenu(),
|
||||
),
|
||||
from = TxHistoryDetailsUM.AssetUM(
|
||||
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
|
||||
|
|
@ -2,26 +2,37 @@ package com.tangem.features.txhistory.ui
|
|||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.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.ds2.button.Close
|
||||
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.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_copy_24
|
||||
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 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.
|
||||
*
|
||||
* 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
|
||||
* 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
|
||||
* 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(
|
||||
modifier = modifier,
|
||||
windowInsets = WindowInsets(0),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
blurBackground = false,
|
||||
startButton = { StatusActionIcon(iconRes = header.iconRes, status = header.status) },
|
||||
startButton = { StatusActionIcon(icon = header.icon, status = header.status) },
|
||||
endButtonsGroup = {
|
||||
// Context menu. Click handling is intentionally not wired yet.
|
||||
TangemButton(
|
||||
variant = TangemButton.Variant.Ghost,
|
||||
iconStart = TangemIconUM.Icon(Icons.ic_dots_horizontal_20),
|
||||
contentDescription = resourceReference(R.string.common_more).resolveReference(),
|
||||
onClick = {},
|
||||
)
|
||||
if (header.menu.isNotEmpty()) {
|
||||
TxHistoryDetailsOverflowMenu(menu = header.menu)
|
||||
}
|
||||
},
|
||||
endButton = { TangemButton.Close(onClick = onCloseClick) },
|
||||
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
|
||||
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(
|
||||
modifier = modifier
|
||||
.size(44.dp)
|
||||
|
|
@ -98,7 +196,7 @@ private fun StatusActionIcon(iconRes: Int, status: Status, modifier: Modifier =
|
|||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(iconRes),
|
||||
imageVector = icon.asImageVector(),
|
||||
contentDescription = null,
|
||||
tint = status.iconTint,
|
||||
modifier = Modifier.size(20.dp),
|
||||
|
|
@ -161,10 +259,24 @@ private fun TxHistoryDetailsTopNavigationPreview() {
|
|||
}
|
||||
|
||||
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,
|
||||
title = title,
|
||||
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
|
||||
|
|
@ -5,10 +5,18 @@ import androidx.compose.ui.graphics.Color
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
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.TxIcon
|
||||
import com.tangem.core.ui.ds.image.DeviceIconUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.ExpressAsset.ID as ExpressAssetId
|
||||
import com.tangem.domain.express.models.ExpressExchangeStatus
|
||||
|
|
@ -128,7 +136,7 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
|
|||
val header = converter.convert(tx).header
|
||||
|
||||
// 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.title).isEqualTo(resourceReference(R.string.common_received))
|
||||
}
|
||||
|
|
@ -202,7 +210,81 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
|
|||
val header = converter.convert(tx).header
|
||||
|
||||
// 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
|
||||
|
|
@ -364,7 +446,7 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
|
|||
|
||||
// Assert
|
||||
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
|
||||
|
|
@ -649,7 +731,7 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
|
|||
val result = converter.convert(expressOnramp(status = ExpressOnrampStatus.Finished)) as TxHistoryDetailsUM.TwoAssets
|
||||
|
||||
// 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(
|
||||
TxHistoryDetailsUM.StatusBannerUM(
|
||||
severity = TxHistoryDetailsUM.StatusBannerUM.Severity.Success,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,15 @@ package com.tangem.features.txhistory.converter
|
|||
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.ContentSubtitle
|
||||
import com.tangem.core.ui.components.transactions.state.TxIcon
|
||||
import com.tangem.core.ui.ds.image.DeviceIconUM
|
||||
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.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -78,7 +85,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
|
|||
val result = coinConverter.convert(tx) as TransactionItemUM.Content
|
||||
|
||||
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
|
||||
|
|
@ -119,7 +126,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
|
|||
assertThat(result.title).isEqualTo(
|
||||
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
|
||||
|
|
@ -193,7 +200,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
|
|||
|
||||
assertThat(result.title).isEqualTo(resRef(R.string.common_sent))
|
||||
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
|
||||
assertThat(subtitle.direction).isEqualTo(ContentSubtitle.Direction.TO)
|
||||
assertThat(subtitle.rawAddress).isEqualTo(USER_ADDRESS)
|
||||
|
|
@ -242,7 +249,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
|
|||
|
||||
assertThat(result.title).isEqualTo(resRef(R.string.common_received))
|
||||
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
|
||||
assertThat(subtitle.direction).isEqualTo(ContentSubtitle.Direction.FROM)
|
||||
}
|
||||
|
|
@ -436,7 +443,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
|
|||
val result = coinConverter.convert(tx) as TransactionItemUM.Content
|
||||
|
||||
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
|
||||
|
|
@ -446,7 +453,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
|
|||
val result = coinConverter.convert(tx) as TransactionItemUM.Content
|
||||
|
||||
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
|
||||
|
|
@ -456,7 +463,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
|
|||
val result = coinConverter.convert(tx) as TransactionItemUM.Content
|
||||
|
||||
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
|
||||
|
|
@ -655,7 +662,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
|
|||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue