Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-24 16:33:01 +03:00
parent ef8a13cfaa
commit 2f60ccf3fc
14 changed files with 732 additions and 148 deletions

View file

@ -2051,6 +2051,8 @@
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="twins_recreate_warning">This action is irreversible. You will not have access to the old wallet.</string>
<string name="twins_scan_twin_with_number">Tap the twin card with number %s and do not remove until the end of the operation</string>
<string name="tx_history_onramp_top_up">Top up</string>
<string name="tx_history_onramp_topped_up">Topped up</string>
<string name="unexpected_error_description">Please try again later. If the issue persists, please contact support.</string>
<string name="unexpected_error_title">Something went wrong!</string>
<string name="universal_error">We\'ve encountered an error. Error code: %s. Please contact our support.</string>

View file

@ -7,6 +7,7 @@ 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -32,6 +33,8 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.components.transactions.state.TransactionItemUM
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Direction
@ -70,55 +73,92 @@ fun TransactionItem(state: TransactionItemUM, isBalanceHidden: Boolean, modifier
@Composable
private fun ContentItem(state: TransactionItemUM.Content, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
val rowModifier = modifier
.fillMaxWidth()
.clickable(onClick = state.onClick)
.testTag(TransactionHistoryItemTestTags.ITEM)
TangemRowContainer(
modifier = rowModifier,
contentPadding = PaddingValues(
horizontal = TangemTheme.dimens2.x4,
vertical = TangemTheme.dimens2.x3,
),
Column(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = state.onClick)
.testTag(TransactionHistoryItemTestTags.ITEM),
) {
StatusCircle(
iconRes = state.iconRes,
status = state.status,
modifier = Modifier
.layoutId(TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x3)
.size(TangemTheme.dimens2.x10)
.testTag(TransactionHistoryItemTestTags.STATUS_PREFIX + state.status.testTagSuffix),
TangemRowContainer(
contentPadding = PaddingValues(
horizontal = TangemTheme.dimens2.x4,
vertical = TangemTheme.dimens2.x3,
),
) {
StatusCircle(
iconRes = state.iconRes,
status = state.status,
modifier = Modifier
.layoutId(TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x3)
.size(TangemTheme.dimens2.x10)
.testTag(TransactionHistoryItemTestTags.STATUS_PREFIX + state.status.testTagSuffix),
)
TitleText(
title = state.title,
status = state.status,
modifier = Modifier
.layoutId(TangemRowLayoutId.START_TOP)
.testTag(TransactionHistoryItemTestTags.TITLE),
)
SubtitleText(
subtitle = state.subtitle,
status = state.status,
modifier = Modifier
.layoutId(TangemRowLayoutId.START_BOTTOM)
.padding(top = TangemTheme.dimens2.x0_5),
)
state.amount?.let { amount ->
AmountText(
amount = amount,
status = state.status,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.layoutId(TangemRowLayoutId.END_TOP)
.testTag(TransactionHistoryItemTestTags.AMOUNT),
)
}
CurrencyText(
symbol = state.currencySymbol,
modifier = Modifier
.layoutId(TangemRowLayoutId.END_BOTTOM)
.padding(top = TangemTheme.dimens2.x0_5)
.testTag(TransactionHistoryItemTestTags.CURRENCY),
)
}
state.warning?.let { warning ->
WarningLine(
warning = warning,
modifier = Modifier.padding(
start = TangemTheme.dimens2.x4,
end = TangemTheme.dimens2.x4,
bottom = TangemTheme.dimens2.x3,
),
)
}
}
}
@Composable
private fun WarningLine(warning: TextReference, modifier: Modifier = Modifier) {
val attention = TangemTheme.colors2.text.status.attention
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
Icon(
painter = painterResource(R.drawable.ic_alert_triangle_20),
contentDescription = null,
tint = attention,
modifier = Modifier.size(TangemTheme.dimens2.x5),
)
TitleText(
title = state.title,
status = state.status,
modifier = Modifier
.layoutId(TangemRowLayoutId.START_TOP)
.testTag(TransactionHistoryItemTestTags.TITLE),
)
SubtitleText(
subtitle = state.subtitle,
status = state.status,
modifier = Modifier
.layoutId(TangemRowLayoutId.START_BOTTOM)
.padding(top = TangemTheme.dimens2.x0_5),
)
AmountText(
amount = state.amount,
status = state.status,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.layoutId(TangemRowLayoutId.END_TOP)
.testTag(TransactionHistoryItemTestTags.AMOUNT),
)
CurrencyText(
symbol = state.currencySymbol,
modifier = Modifier
.layoutId(TangemRowLayoutId.END_BOTTOM)
.padding(top = TangemTheme.dimens2.x0_5)
.testTag(TransactionHistoryItemTestTags.CURRENCY),
Text(
text = warning.resolveReference(),
color = attention,
style = TangemTheme.typography2.captionMedium12,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
@ -262,6 +302,21 @@ private fun SubtitleText(subtitle: ContentSubtitle, status: Status, modifier: Mo
modifier = Modifier.fillMaxSize(),
)
}
is ContentSubtitle.Asset -> InlineImageSubtitle(
template = stringResourceSafe(subtitle.direction.templateResId(), subtitle.symbol),
color = tertiary,
afterIconColor = if (isFailed) tertiary else primary,
modifier = modifier,
) {
subtitle.icon?.let { iconState ->
CurrencyIcon(
state = iconState,
shouldDisplayNetwork = false,
withFixedSize = false,
modifier = Modifier.fillMaxSize(),
)
}
}
}
}
@ -491,4 +546,73 @@ private fun Preview_TransactionItem_Swap() {
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_TransactionItem_Express() {
TangemThemePreviewRedesign {
PreviewColumn(
items = listOf(
TransactionItemUM.Content(
txHash = "exp-swap-u",
amount = "-390.00",
currencySymbol = "USDT",
time = "",
status = Status.Unconfirmed,
direction = Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_exchange_vertical_24,
title = stringReference("Swapping"),
subtitle = ContentSubtitle.Asset(
direction = ContentSubtitle.Direction.TO,
symbol = "POL",
icon = CurrencyIconState.CoinIcon(
url = null,
fallbackResId = R.drawable.ic_custom_token_44,
isGrayscale = false,
shouldShowCustomBadge = false,
),
),
timestamp = 0L,
warning = stringReference("KYC verification required by provider"),
),
TransactionItemUM.Content(
txHash = "exp-onramp-c",
amount = "+0.006339",
currencySymbol = "BTC",
time = "",
status = Status.Confirmed,
direction = Direction.INCOMING,
onClick = {},
iconRes = R.drawable.ic_tangem_card_24,
title = stringReference("Topped up"),
subtitle = ContentSubtitle.Asset(
direction = ContentSubtitle.Direction.FROM,
symbol = "SEK",
icon = null,
),
timestamp = 0L,
),
TransactionItemUM.Content(
txHash = "exp-onramp-f",
amount = "0.006339",
currencySymbol = "BTC",
time = "",
status = Status.Failed,
direction = Direction.INCOMING,
onClick = {},
iconRes = R.drawable.ic_tangem_card_24,
title = stringReference("Top up failed"),
subtitle = ContentSubtitle.Asset(
direction = ContentSubtitle.Direction.FROM,
symbol = "SEK",
icon = null,
),
timestamp = 0L,
),
),
)
}
}
// endregion

View file

@ -3,6 +3,7 @@ package com.tangem.core.ui.components.transactions.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.ds.image.DeviceIconUM
import com.tangem.core.ui.extensions.TextReference
@ -22,12 +23,13 @@ sealed interface TransactionItemUM {
/**
* Content state.
*
* @property amount signed numeric value, e.g. "+0.500913" / "-350.31"; no currency symbol embedded
* @property amount signed numeric value, e.g. "+0.500913" / "-350.31"; no currency symbol embedded.
* `null` hides the numeric value while [currencySymbol] still shows.
* @property currencySymbol currency symbol shown alongside [amount], e.g. "BTC", "USDT"
*/
data class Content(
override val txHash: String,
val amount: String,
val amount: String?,
val currencySymbol: String,
val time: String,
val status: Status,
@ -37,6 +39,7 @@ sealed interface TransactionItemUM {
val title: TextReference,
val subtitle: ContentSubtitle,
val timestamp: Long,
val warning: TextReference? = null,
) : TransactionItemUM {
@Immutable
@ -95,6 +98,19 @@ sealed interface TransactionItemUM {
val deviceIconUM: DeviceIconUM,
) : ContentSubtitle
/**
* Counterparty asset ticker renders as "to/from: <icon> <SYMBOL>". Used for express rows
* (swap counterparty currency / onramp fiat), e.g. "to: ◎ POL" or "from: 🇸🇪 SEK".
*
* @property icon resolved counterparty currency icon, rendered via `CurrencyIcon`. `null` when no icon
* is available (e.g. onramp fiat carries no `CryptoCurrency`) the ticker then renders without a leading icon.
*/
data class Asset(
val direction: Direction,
val symbol: String,
val icon: CurrencyIconState?,
) : ContentSubtitle
enum class Direction { TO, FROM }
}