Updated on 2026-08-14
This commit is contained in:
commit
39635f2b3d
8 changed files with 805 additions and 14 deletions
|
|
@ -14,11 +14,13 @@ 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.features.txhistory.entity.TxHistoryDetailsUM
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM.StatusBannerUM.Severity
|
||||
import com.tangem.features.txhistory.impl.R
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import com.tangem.utils.toBriefAddressFormat
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.joda.time.DateTime
|
||||
|
||||
/**
|
||||
|
|
@ -37,13 +39,18 @@ internal class TxInfoToTxHistoryDetailsUMConverter(
|
|||
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
|
||||
|
||||
override fun convert(value: TxInfo): TxHistoryDetailsUM = when (value.type) {
|
||||
is TransactionType.Swap -> TxHistoryDetailsUM.TwoAssets(header = value.toHeaderUM())
|
||||
// TODO([REDACTED_TASK_KEY]): populate `from` / `to` legs once TxInfo exposes the swap legs (amounts, currencies, fiat).
|
||||
// Until then the card falls back to the header-only placeholder (the TwoAssetsBlock UI is already wired).
|
||||
is TransactionType.Swap -> TxHistoryDetailsUM.TwoAssets(
|
||||
header = value.toHeaderUM(),
|
||||
statusBanner = value.toStatusBannerUM(),
|
||||
)
|
||||
else -> TxHistoryDetailsUM.SingleAsset(
|
||||
header = value.toHeaderUM(),
|
||||
amountBlock = value.toAmountBlockUM(),
|
||||
counterparty = value.toCounterpartyUM(),
|
||||
// TODO: TxInfo has no network fee / rate yet — empty until those fields are added to TxInfo.
|
||||
rows = emptyList(),
|
||||
rows = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -54,6 +61,31 @@ internal class TxInfoToTxHistoryDetailsUMConverter(
|
|||
subtitle = headerSubtitle(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Express status plaque under the swap block. A stopgap over the three generic [TxInfo.TransactionStatus] values —
|
||||
* so [Severity.Warning] (verification) is not reachable yet.
|
||||
*
|
||||
* [REDACTED_TODO_COMMENT]
|
||||
*/
|
||||
private fun TxInfo.toStatusBannerUM(): TxHistoryDetailsUM.StatusBannerUM = when (status) {
|
||||
is TxInfo.TransactionStatus.Unconfirmed -> TxHistoryDetailsUM.StatusBannerUM(
|
||||
severity = Severity.Info,
|
||||
title = resourceReference(R.string.express_exchange_status_receiving_active),
|
||||
isLoading = true,
|
||||
)
|
||||
is TxInfo.TransactionStatus.Confirmed -> TxHistoryDetailsUM.StatusBannerUM(
|
||||
severity = Severity.Success,
|
||||
title = resourceReference(R.string.express_exchange_status_exchanged),
|
||||
isLoading = false,
|
||||
)
|
||||
is TxInfo.TransactionStatus.Failed -> TxHistoryDetailsUM.StatusBannerUM(
|
||||
severity = Severity.Error,
|
||||
title = resourceReference(R.string.express_exchange_status_failed),
|
||||
subtitle = resourceReference(R.string.express_exchange_notification_failed_text),
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TxInfo.toAmountBlockUM(): TxHistoryDetailsUM.AmountBlockUM = TxHistoryDetailsUM.AmountBlockUM(
|
||||
currencyIcon = iconStateConverter.convert(currency),
|
||||
amount = stringReference(signedAmount(currency)),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
|||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM
|
||||
import com.tangem.core.ui.ds.image.DeviceIconUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* UI model for the in-app transaction details ("Operation") card.
|
||||
|
|
@ -28,14 +29,79 @@ internal sealed interface TxHistoryDetailsUM : TangemBottomSheetConfigContent {
|
|||
override val header: HeaderUM,
|
||||
val amountBlock: AmountBlockUM,
|
||||
val counterparty: CounterpartyUM?,
|
||||
val rows: List<InfoRowUM>,
|
||||
val rows: ImmutableList<InfoRowUM>,
|
||||
) : TxHistoryDetailsUM
|
||||
|
||||
/** Two-asset layout: Swap / Onramp */
|
||||
/**
|
||||
* Two-asset layout: Swap / Onramp.
|
||||
*
|
||||
* [from] ("You sent") → [to] ("You receive") exchange block. Both are nullable: the converter can't populate the
|
||||
* legs yet (`TxInfo` exposes no swap amounts/currencies/fiat), so the card falls back to a header-only placeholder
|
||||
* until that data lands. [statusBanner] is the express status plaque under the block, `null` until status is known.
|
||||
*/
|
||||
data class TwoAssets(
|
||||
override val header: HeaderUM,
|
||||
val from: AssetUM? = null,
|
||||
val to: AssetUM? = null,
|
||||
val statusBanner: StatusBannerUM? = null,
|
||||
) : TxHistoryDetailsUM
|
||||
|
||||
/**
|
||||
* Express status plaque under the two-asset block. The UI animates between successive emissions.
|
||||
*
|
||||
* @property severity Plaque colors (background tint + text/icon color).
|
||||
* @property title Status line, e.g. "Awaiting funds" / "Confirmed" / "Failed".
|
||||
* @property subtitle Optional second line (e.g. the refund hint on a failed terminal).
|
||||
* @property isLoading `true` → trailing rotating loader (in-progress); `false` → static [severity] glyph.
|
||||
*/
|
||||
data class StatusBannerUM(
|
||||
val severity: Severity,
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference? = null,
|
||||
val isLoading: Boolean,
|
||||
) {
|
||||
|
||||
/** Visual severity of the [StatusBannerUM] — selects the background tint and the text/icon color. */
|
||||
enum class Severity { Info, Success, Error, Warning }
|
||||
}
|
||||
|
||||
/**
|
||||
* One side of the two-asset block: the [label] over the signed [amount], with the [currencyIcon] on the trailing
|
||||
* side. [owner] `null` → plain label ("You sent"); non-null → "From"/"To" prefix plus the resolved own account /
|
||||
* wallet decoration. [isFaded] renders the unsettled/failed amount (struck through, recolored to tertiary).
|
||||
*/
|
||||
data class AssetUM(
|
||||
val label: TextReference,
|
||||
val owner: AssetOwnerUM?,
|
||||
val amount: TextReference,
|
||||
val currencyIcon: CurrencyIconState,
|
||||
val isFaded: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
* Counterparty rendered inline in an [AssetUM.label] when a swap leg resolves to one of the user's own portfolios.
|
||||
* Carries the [name] plus a kind-specific 16dp decoration. Only own account / own wallet are decorated here (no
|
||||
* address case, unlike the single-asset [CounterpartyAvatar]).
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface AssetOwnerUM {
|
||||
|
||||
val name: TextReference
|
||||
|
||||
/** User's own account — the [iconResId] glyph tinted over [backgroundColor], shown **before** the [name]. */
|
||||
data class Account(
|
||||
override val name: TextReference,
|
||||
@DrawableRes val iconResId: Int,
|
||||
val backgroundColor: Color,
|
||||
) : AssetOwnerUM
|
||||
|
||||
/** User's own wallet — the wallet card [deviceIconUM], shown **after** the [name]. */
|
||||
data class Wallet(
|
||||
override val name: TextReference,
|
||||
val deviceIconUM: DeviceIconUM,
|
||||
) : AssetOwnerUM
|
||||
}
|
||||
|
||||
/**
|
||||
* Centered amount block of the single-asset card: token avatar (with network badge), the big signed crypto
|
||||
* [amount] and the secondary [fiatAmount].
|
||||
|
|
@ -43,7 +109,6 @@ internal sealed interface TxHistoryDetailsUM : TangemBottomSheetConfigContent {
|
|||
* [isFailed] drives the failed visual state — the amount is struck through, recolored to tertiary and carries no
|
||||
* `+`/`−` sign (mirrors the status-driven recolor in the shared header).
|
||||
*/
|
||||
@Immutable
|
||||
data class AmountBlockUM(
|
||||
val currencyIcon: CurrencyIconState,
|
||||
val amount: TextReference,
|
||||
|
|
@ -55,7 +120,6 @@ internal sealed interface TxHistoryDetailsUM : TangemBottomSheetConfigContent {
|
|||
* A single info row of the details card: a [label] on the leading side and its [value] on the trailing side
|
||||
* (e.g. `Network fee` → `0.00056 ETH`, `Rate` → `1 POL ≈ 0.36 USDT`). Rendered by [TxHistoryDetailsInfoRows].
|
||||
*/
|
||||
@Immutable
|
||||
data class InfoRowUM(
|
||||
val label: TextReference,
|
||||
val value: TextReference,
|
||||
|
|
@ -76,7 +140,6 @@ internal sealed interface TxHistoryDetailsUM : TangemBottomSheetConfigContent {
|
|||
* @property avatar Leading avatar.
|
||||
* @property onCopyClick Copy action; `null` hides the copy button (e.g. own-wallet has nothing to copy).
|
||||
*/
|
||||
@Immutable
|
||||
data class CounterpartyUM(
|
||||
val label: TextReference,
|
||||
val title: TextReference,
|
||||
|
|
@ -105,7 +168,6 @@ 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.
|
||||
*/
|
||||
@Immutable
|
||||
data class HeaderUM(
|
||||
@DrawableRes val iconRes: Int,
|
||||
val status: TransactionItemUM.Content.Status,
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ import com.tangem.features.txhistory.entity.TxHistoryDetailsUM
|
|||
internal fun TxHistoryDetailsContent(state: TxHistoryDetailsUM, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is TxHistoryDetailsUM.SingleAsset -> SingleAssetContent(state = state, modifier = modifier)
|
||||
// TODO([REDACTED_TASK_KEY]): two-asset (Swap / Onramp) body — out of scope for the single-asset amount block ticket.
|
||||
is TxHistoryDetailsUM.TwoAssets -> TwoAssetsPlaceholder(state = state, modifier = modifier)
|
||||
is TxHistoryDetailsUM.TwoAssets -> TwoAssetsContent(state = state, modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +44,35 @@ private fun SingleAssetContent(state: TxHistoryDetailsUM.SingleAsset, modifier:
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TwoAssetsContent(state: TxHistoryDetailsUM.TwoAssets, modifier: Modifier = Modifier) {
|
||||
val from = state.from
|
||||
val to = state.to
|
||||
Column(modifier = modifier.fillMaxWidth().padding(bottom = 16.dp)) {
|
||||
if (from != null && to != null) {
|
||||
TxHistoryDetailsTwoAssetsBlock(
|
||||
from = from,
|
||||
to = to,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp),
|
||||
)
|
||||
} else {
|
||||
// TODO([REDACTED_TASK_KEY]): the converter cannot populate the swap legs yet (TxInfo exposes no two-leg / fiat /
|
||||
// provider data). Until those fields land, fall back to the header-only placeholder.
|
||||
TwoAssetsPlaceholder(state = state)
|
||||
}
|
||||
// Express status plaque under the exchange block. The top gap is owned by the banner (inside its collapsing
|
||||
// region), so only horizontal padding is applied here.
|
||||
TxHistoryDetailsStatusBanner(
|
||||
state = state.statusBanner,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TwoAssetsPlaceholder(state: TxHistoryDetailsUM.TwoAssets, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM.InfoRowUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* Info-rows block of the transaction details card: a vertical list of DS3 [TangemRow]s (label on the leading side,
|
||||
|
|
@ -36,7 +38,7 @@ import com.tangem.features.txhistory.entity.TxHistoryDetailsUM.InfoRowUM
|
|||
* @param modifier Modifier applied to the list container.
|
||||
*/
|
||||
@Composable
|
||||
internal fun TxHistoryDetailsInfoRows(rows: List<InfoRowUM>, modifier: Modifier = Modifier) {
|
||||
internal fun TxHistoryDetailsInfoRows(rows: ImmutableList<InfoRowUM>, modifier: Modifier = Modifier) {
|
||||
if (rows.isEmpty()) return
|
||||
Column(
|
||||
modifier = modifier,
|
||||
|
|
@ -74,7 +76,7 @@ private fun TxHistoryDetailsInfoRowsPreview() {
|
|||
) {
|
||||
// Multiple rows — dividers between rows, none after the last
|
||||
TxHistoryDetailsInfoRows(
|
||||
rows = listOf(
|
||||
rows = persistentListOf(
|
||||
InfoRowUM(label = stringReference("Network fee"), value = stringReference("0.00056 ETH")),
|
||||
InfoRowUM(label = stringReference("Rate"), value = stringReference("1 POL ≈ 0.36 USDT")),
|
||||
InfoRowUM(label = stringReference("Network fee"), value = stringReference("0.00056 ETH")),
|
||||
|
|
@ -83,7 +85,7 @@ private fun TxHistoryDetailsInfoRowsPreview() {
|
|||
// Single row — no divider
|
||||
TxHistoryDetailsInfoRows(
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
rows = listOf(
|
||||
rows = persistentListOf(
|
||||
InfoRowUM(label = stringReference("Network fee"), value = stringReference("0.00056 ETH")),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* The transaction details bottom sheet ("Operation"): the [TangemModalBottomSheet] shell shared by all transaction
|
||||
|
|
@ -79,7 +80,7 @@ private fun previewSingleAsset() = TxHistoryDetailsUM.SingleAsset(
|
|||
),
|
||||
onCopyClick = {},
|
||||
),
|
||||
rows = listOf(
|
||||
rows = persistentListOf(
|
||||
TxHistoryDetailsUM.InfoRowUM(label = stringReference("Network fee"), value = stringReference("0.00056 ETH")),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,311 @@
|
|||
package com.tangem.features.txhistory.ui
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.SizeTransform
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.ds2.loader.TangemLoader
|
||||
import com.tangem.core.ui.ds2.loader.TangemLoaderSize
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
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_error_20
|
||||
import com.tangem.core.ui.res.generated.icons.ic_info_20
|
||||
import com.tangem.core.ui.res.generated.icons.ic_success_20
|
||||
import com.tangem.core.ui.res.generated.icons.ic_warning_20
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM.StatusBannerUM
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM.StatusBannerUM.Severity
|
||||
|
||||
// Animation timings in ms (ProtoPie spec). The status swap is two-phase: the old status fades out, then the new one
|
||||
// fades/slides in after ENTER_DELAY. Most steps run over the default duration; the trailing loader/glyph fades faster
|
||||
// (FAST_FADE), and the plaque grows over GROW to make room for a subtitle.
|
||||
private const val DEFAULT_ANIMATION_MILLIS = 300
|
||||
private const val FAST_FADE_MILLIS = 200
|
||||
private const val GROW_MILLIS = 400
|
||||
private const val ENTER_DELAY_MILLIS = DEFAULT_ANIMATION_MILLIS // phase 2 waits for the phase-1 fade-out to clear
|
||||
private const val SUBTITLE_DELAY_MILLIS = ENTER_DELAY_MILLIS + 100 // subtitle trails the title
|
||||
|
||||
private const val TITLE_SLIDE_FRACTION = 12 // in-progress/Success title slides in 1/12 width from the right
|
||||
private const val CONTENT_RISE_FRACTION = 2 // Warning/Error title floats up 1/2 height from below
|
||||
private const val ICON_ENTER_SCALE = 0.6f
|
||||
|
||||
/** Gap between the exchange block above and the plaque; kept inside the collapsing region so it folds away cleanly. */
|
||||
private val BANNER_TOP_GAP = 12.dp
|
||||
|
||||
/** Gap between the title row and the subtitle; lives inside the subtitle slot so it folds away when there's no line. */
|
||||
private val SUBTITLE_TOP_GAP = 4.dp
|
||||
|
||||
/** Key for the title [AnimatedContent]: the resolved [text] plus the [severity] that selects the swap motion. */
|
||||
private data class StatusBannerTitle(val text: String, val severity: Severity)
|
||||
|
||||
/**
|
||||
* Title transition picked by the *target* severity: Info/Success slide in from the right ([titleSlide]); Warning/Error
|
||||
* float up from below ([titleRise]). Both fade the old status out fully before fading the new one in.
|
||||
*/
|
||||
private fun titleTransition(target: Severity): ContentTransform = when (target) {
|
||||
Severity.Warning, Severity.Error -> titleRise()
|
||||
Severity.Info, Severity.Success -> titleSlide()
|
||||
}
|
||||
|
||||
/** In-progress / success swap: old status fades out, new one fades in sliding from the right. */
|
||||
private fun titleSlide(): ContentTransform = ContentTransform(
|
||||
targetContentEnter = fadeIn(tween(durationMillis = DEFAULT_ANIMATION_MILLIS, delayMillis = ENTER_DELAY_MILLIS)) +
|
||||
slideInHorizontally(
|
||||
animationSpec = tween(durationMillis = DEFAULT_ANIMATION_MILLIS, delayMillis = ENTER_DELAY_MILLIS),
|
||||
) { width -> width / TITLE_SLIDE_FRACTION },
|
||||
initialContentExit = fadeOut(tween(durationMillis = DEFAULT_ANIMATION_MILLIS)),
|
||||
sizeTransform = SizeTransform(clip = false) { _, _ -> snap() },
|
||||
)
|
||||
|
||||
/** Terminal warning / error swap: old status fades out, new one fades in floating up a touch from below. */
|
||||
private fun titleRise(): ContentTransform = ContentTransform(
|
||||
targetContentEnter = fadeIn(tween(durationMillis = DEFAULT_ANIMATION_MILLIS, delayMillis = ENTER_DELAY_MILLIS)) +
|
||||
slideInVertically(
|
||||
animationSpec = tween(durationMillis = DEFAULT_ANIMATION_MILLIS, delayMillis = ENTER_DELAY_MILLIS),
|
||||
) { height -> height / CONTENT_RISE_FRACTION },
|
||||
initialContentExit = fadeOut(tween(durationMillis = DEFAULT_ANIMATION_MILLIS)),
|
||||
sizeTransform = SizeTransform(clip = false) { _, _ -> snap() },
|
||||
)
|
||||
|
||||
/** Trailing-slot swap (loader → glyph): loader fades out (Phase 1), then the glyph "pops" in (Phase 2). */
|
||||
private fun iconSwapTransition(): ContentTransform = ContentTransform(
|
||||
targetContentEnter = fadeIn(tween(durationMillis = FAST_FADE_MILLIS, delayMillis = ENTER_DELAY_MILLIS)) +
|
||||
scaleIn(
|
||||
animationSpec = tween(durationMillis = DEFAULT_ANIMATION_MILLIS, delayMillis = ENTER_DELAY_MILLIS),
|
||||
initialScale = ICON_ENTER_SCALE,
|
||||
),
|
||||
initialContentExit = fadeOut(tween(durationMillis = FAST_FADE_MILLIS)),
|
||||
sizeTransform = SizeTransform(clip = false) { _, _ -> snap() },
|
||||
)
|
||||
|
||||
/**
|
||||
* Express status plaque of the Swap / Onramp transaction details, rendered under the two-asset exchange block.
|
||||
*
|
||||
* [Figma](https://www.figma.com/design/Qqm0dNTOnqtxLYEcmgc32C/Store?node-id=1370-114172)
|
||||
*
|
||||
* Two animation layers: [AnimatedVisibility] grows the plaque in from its top edge / collapses it to the bottom;
|
||||
* in-place status transitions ([StatusBannerContent]) morph the title, background tint and trailing loader→glyph as
|
||||
* the model re-emits the latest [state].
|
||||
*
|
||||
* @param state Current status to render, or `null` to hide the plaque (animated out).
|
||||
* @param modifier Modifier applied to the plaque container.
|
||||
*/
|
||||
@Composable
|
||||
internal fun TxHistoryDetailsStatusBanner(state: StatusBannerUM?, modifier: Modifier = Modifier) {
|
||||
// Retain the last non-null state so content stays rendered through the exit (collapse+fade). The retained value
|
||||
// only backfills the exit (when [state] is null); published in a SideEffect, not written during composition.
|
||||
val lastState = remember { mutableStateOf<StatusBannerUM?>(null) }
|
||||
SideEffect { if (state != null) lastState.value = state }
|
||||
val content = state ?: lastState.value
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state != null,
|
||||
// Fade and size share one tween so alpha and height finish together (mismatched default springs leave a jerk).
|
||||
enter = fadeIn(tween(DEFAULT_ANIMATION_MILLIS)) +
|
||||
expandVertically(tween(DEFAULT_ANIMATION_MILLIS), expandFrom = Alignment.Top),
|
||||
exit = fadeOut(tween(DEFAULT_ANIMATION_MILLIS)) +
|
||||
shrinkVertically(tween(DEFAULT_ANIMATION_MILLIS), shrinkTowards = Alignment.Bottom),
|
||||
modifier = modifier,
|
||||
) {
|
||||
// Leading gap lives inside the animated region so it collapses together with the plaque (no residual margin).
|
||||
content?.let { StatusBannerContent(state = it, modifier = Modifier.padding(top = BANNER_TOP_GAP)) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusBannerContent(state: StatusBannerUM, modifier: Modifier = Modifier) {
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = state.severity.backgroundColor(),
|
||||
// Delayed into Phase 2, so the tint starts shifting only once the old title has faded out, matching the spec.
|
||||
animationSpec = tween(durationMillis = DEFAULT_ANIMATION_MILLIS, delayMillis = ENTER_DELAY_MILLIS),
|
||||
label = "StatusBannerBackground",
|
||||
)
|
||||
val contentColor = state.severity.contentColor()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(backgroundColor)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// Animate the title as the status advances. Keyed on (text, severity) so [titleTransition] picks the motion
|
||||
// by target; the key also colors each content from its own severity (see [color] below).
|
||||
AnimatedContent(
|
||||
targetState = StatusBannerTitle(state.title.resolveReference(), state.severity),
|
||||
transitionSpec = { titleTransition(target = targetState.severity) },
|
||||
label = "StatusBannerTitle",
|
||||
modifier = Modifier.weight(1f),
|
||||
) { title ->
|
||||
Text(
|
||||
text = title.text,
|
||||
style = TangemTheme.typography3.body.medium,
|
||||
// From this title's own key, so the outgoing title fades out in its colour instead of snapping.
|
||||
color = title.severity.contentColor(),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
StatusBannerTrailing(isLoading = state.isLoading, severity = state.severity)
|
||||
}
|
||||
// Retain the last non-null subtitle so the line stays rendered while it fades out (mirrors the retain above).
|
||||
val lastSubtitle = remember { mutableStateOf<TextReference?>(null) }
|
||||
SideEffect { if (state.subtitle != null) lastSubtitle.value = state.subtitle }
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state.subtitle != null,
|
||||
// The subtitle owns the plaque's growth: expandVertically opens its slot in Phase 2, then the text fades in
|
||||
// a touch later so it trails the title. expandVertically (not animateContentSize) lets us delay the growth.
|
||||
enter = expandVertically(
|
||||
animationSpec = tween(GROW_MILLIS, delayMillis = ENTER_DELAY_MILLIS),
|
||||
expandFrom = Alignment.Top,
|
||||
) + fadeIn(tween(DEFAULT_ANIMATION_MILLIS, delayMillis = SUBTITLE_DELAY_MILLIS)),
|
||||
exit = shrinkVertically(tween(DEFAULT_ANIMATION_MILLIS), shrinkTowards = Alignment.Top) +
|
||||
fadeOut(tween(DEFAULT_ANIMATION_MILLIS)),
|
||||
) {
|
||||
(state.subtitle ?: lastSubtitle.value)?.let { subtitle ->
|
||||
Text(
|
||||
text = subtitle.resolveReference(),
|
||||
style = TangemTheme.typography3.caption.medium,
|
||||
color = contentColor,
|
||||
modifier = Modifier.padding(top = SUBTITLE_TOP_GAP),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Key for the trailing [AnimatedContent]: whether the loader or a glyph shows, plus the [severity] that tints it. */
|
||||
private data class StatusBannerGlyph(val isLoading: Boolean, val severity: Severity)
|
||||
|
||||
/** Trailing slot: rotating loader while in progress, the static severity status glyph once terminal. */
|
||||
@Composable
|
||||
private fun StatusBannerTrailing(isLoading: Boolean, severity: Severity, modifier: Modifier = Modifier) {
|
||||
// Keyed on (isLoading, severity) so the tint comes from each content's own key — the outgoing loader then fades
|
||||
// out in its colour instead of snapping to the incoming status'.
|
||||
AnimatedContent(
|
||||
targetState = StatusBannerGlyph(isLoading, severity),
|
||||
transitionSpec = { iconSwapTransition() },
|
||||
label = "StatusBannerTrailing",
|
||||
modifier = modifier,
|
||||
) { glyph ->
|
||||
val tint = glyph.severity.contentColor()
|
||||
if (glyph.isLoading) {
|
||||
TangemLoader(size = TangemLoaderSize.X20, color = tint)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = glyph.severity.statusIcon(),
|
||||
contentDescription = null,
|
||||
tint = tint,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Severity.backgroundColor(): Color = when (this) {
|
||||
Severity.Info -> TangemTheme.colors3.bg.status.infoSubtle
|
||||
Severity.Success -> TangemTheme.colors3.bg.status.successSubtle
|
||||
Severity.Error -> TangemTheme.colors3.bg.status.errorSubtle
|
||||
Severity.Warning -> TangemTheme.colors3.bg.status.warningSubtle
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Severity.contentColor(): Color = when (this) {
|
||||
Severity.Info -> TangemTheme.colors3.text.status.info
|
||||
Severity.Success -> TangemTheme.colors3.text.status.success
|
||||
Severity.Error -> TangemTheme.colors3.text.status.error
|
||||
Severity.Warning -> TangemTheme.colors3.text.status.warning
|
||||
}
|
||||
|
||||
private fun Severity.statusIcon() = when (this) {
|
||||
Severity.Success -> Icons.ic_success_20
|
||||
Severity.Error -> Icons.ic_error_20
|
||||
Severity.Warning -> Icons.ic_warning_20
|
||||
Severity.Info -> Icons.ic_info_20
|
||||
}
|
||||
|
||||
// region Preview
|
||||
|
||||
@Preview(name = "Light", showBackground = true, widthDp = 360)
|
||||
@Preview(name = "Dark", uiMode = UI_MODE_NIGHT_YES, showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun TxHistoryDetailsStatusBannerPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors3.bg.primary)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
TxHistoryDetailsStatusBanner(
|
||||
state = StatusBannerUM(Severity.Info, stringReference("Awaiting funds"), isLoading = true),
|
||||
)
|
||||
TxHistoryDetailsStatusBanner(
|
||||
state = StatusBannerUM(Severity.Info, stringReference("Deposit confirmed"), isLoading = true),
|
||||
)
|
||||
TxHistoryDetailsStatusBanner(
|
||||
state = StatusBannerUM(Severity.Success, stringReference("Confirmed"), isLoading = false),
|
||||
)
|
||||
TxHistoryDetailsStatusBanner(
|
||||
state = StatusBannerUM(
|
||||
severity = Severity.Error,
|
||||
title = stringReference("Failed"),
|
||||
subtitle = stringReference("Visit provider's website to refund your money"),
|
||||
isLoading = false,
|
||||
),
|
||||
)
|
||||
TxHistoryDetailsStatusBanner(
|
||||
state = StatusBannerUM(
|
||||
severity = Severity.Warning,
|
||||
title = stringReference("Verification required"),
|
||||
subtitle = stringReference("Visit provider's website to refund your money"),
|
||||
isLoading = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
package com.tangem.features.txhistory.ui
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
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.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.currency.icon.TangemCurrencyIcon
|
||||
import com.tangem.core.ui.ds.image.DeviceIconUM
|
||||
import com.tangem.core.ui.ds.image.TangemDeviceIcon
|
||||
import com.tangem.core.ui.ds2.row.TangemRow
|
||||
import com.tangem.core.ui.ds2.row.TangemRowContentLead
|
||||
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM.AssetOwnerUM
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM.AssetUM
|
||||
|
||||
/**
|
||||
* Two-asset ("exchange") block of the details card, used by Swap (and later Onramp): one `bg.tertiary` rounded cell
|
||||
* with the [from] ("You sent") side over the [to] ("You receive") side, split by an inset dashed divider with a
|
||||
* centered down-arrow masking the line. Each side is a [TangemRow]: label over the signed amount, avatar trailing.
|
||||
*
|
||||
* [Figma](https://www.figma.com/design/Qqm0dNTOnqtxLYEcmgc32C/Store?node-id=1265-87546)
|
||||
*
|
||||
* @param from Sent ("You sent" / "From …") side.
|
||||
* @param to Received ("You receive" / "To …") side.
|
||||
* @param modifier Modifier applied to the block container.
|
||||
*/
|
||||
@Composable
|
||||
internal fun TxHistoryDetailsTwoAssetsBlock(from: AssetUM, to: AssetUM, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(TangemTheme.colors3.bg.tertiary),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
TwoAssetsSideRow(asset = from)
|
||||
DashedDivider()
|
||||
TwoAssetsSideRow(asset = to)
|
||||
}
|
||||
// Centered exchange arrow. Both rows are equal-height, so the block center sits on the divider; the
|
||||
// `bg.tertiary` chip behind the icon masks the dashed line, reproducing the Figma center gap.
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.clip(CircleShape)
|
||||
.background(TangemTheme.colors3.bg.tertiary)
|
||||
.padding(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_arrow_down_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors3.icon.secondary,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TwoAssetsSideRow(asset: AssetUM, modifier: Modifier = Modifier) {
|
||||
TangemRow(
|
||||
modifier = modifier,
|
||||
contentLead = TangemRowContentLead.Start,
|
||||
verticalAlignment = TangemRowVerticalAlignment.Center,
|
||||
titleSlot = { TwoAssetsSideLabel(label = asset.label, owner = asset.owner) },
|
||||
subtitleSlot = {
|
||||
Text(
|
||||
text = asset.amount.resolveReference(),
|
||||
style = TangemTheme.typography3.heading.small,
|
||||
color = if (asset.isFaded) {
|
||||
TangemTheme.colors3.text.tertiary
|
||||
} else {
|
||||
TangemTheme.colors3.text.primary
|
||||
},
|
||||
textDecoration = if (asset.isFaded) TextDecoration.LineThrough else null,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(top = 6.dp),
|
||||
)
|
||||
},
|
||||
endSlot = {
|
||||
TangemCurrencyIcon(
|
||||
state = asset.currencyIcon,
|
||||
modifier = Modifier.size(40.dp),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Caption label above a leg amount. Renders the [label] prefix ("You sent" / "You receive", or "From" / "To" when an
|
||||
* [owner] is present) and, for a resolved [owner], its inline 16dp decoration in the Figma order — the account avatar
|
||||
* leads its name, the wallet key-card icon trails its name.
|
||||
*/
|
||||
@Composable
|
||||
private fun TwoAssetsSideLabel(label: TextReference, owner: AssetOwnerUM?, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
LabelText(text = label)
|
||||
when (owner) {
|
||||
is AssetOwnerUM.Account -> {
|
||||
AssetOwnerIcon(owner = owner)
|
||||
LabelText(text = owner.name, modifier = Modifier.weight(weight = 1f, fill = false))
|
||||
}
|
||||
is AssetOwnerUM.Wallet -> {
|
||||
LabelText(text = owner.name, modifier = Modifier.weight(weight = 1f, fill = false))
|
||||
AssetOwnerIcon(owner = owner)
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LabelText(text: TextReference, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = text.resolveReference(),
|
||||
style = TangemTheme.typography3.caption.medium,
|
||||
color = TangemTheme.colors3.text.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
/** 16dp inline owner decoration: the account glyph over its color, or the wallet device card. */
|
||||
@Composable
|
||||
private fun AssetOwnerIcon(owner: AssetOwnerUM, modifier: Modifier = Modifier) {
|
||||
val iconModifier = modifier.size(16.dp)
|
||||
when (owner) {
|
||||
is AssetOwnerUM.Account -> Box(
|
||||
modifier = iconModifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(owner.backgroundColor),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = owner.iconResId),
|
||||
contentDescription = null,
|
||||
// staticDark == white in both themes (the constant glyph tone for a colored avatar), matching the
|
||||
// white-on-color account avatar in Figma and the counterparty card / history-list account icon.
|
||||
tint = TangemTheme.colors3.icon.staticDark,
|
||||
modifier = Modifier.size(8.dp),
|
||||
)
|
||||
}
|
||||
is AssetOwnerUM.Wallet -> TangemDeviceIcon(
|
||||
state = owner.deviceIconUM,
|
||||
modifier = iconModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** 1px inset dashed divider between the two sides, matching the Figma `divider` (dashed `line`). */
|
||||
@Composable
|
||||
private fun DashedDivider(modifier: Modifier = Modifier) {
|
||||
val color = TangemTheme.colors3.border.tertiary
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.height(1.dp)
|
||||
.drawBehind {
|
||||
val stroke = 1.dp.toPx()
|
||||
val y = size.height / 2f
|
||||
drawLine(
|
||||
color = color,
|
||||
start = Offset(x = 0f, y = y),
|
||||
end = Offset(x = size.width, y = y),
|
||||
strokeWidth = stroke,
|
||||
cap = StrokeCap.Round,
|
||||
pathEffect = PathEffect.dashPathEffect(
|
||||
intervals = floatArrayOf(2.dp.toPx(), 4.dp.toPx()),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Preview(name = "Light", showBackground = true, widthDp = 360)
|
||||
@Preview(name = "Dark", uiMode = UI_MODE_NIGHT_YES, showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun TxHistoryDetailsTwoAssetsBlockPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors3.bg.primary)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
// Plain swap (no resolved owner) — both sides settled.
|
||||
TxHistoryDetailsTwoAssetsBlock(
|
||||
from = previewAsset(label = "You sent", amount = "- 390 USDT", isFaded = false),
|
||||
to = previewAsset(label = "You receive", amount = "+ 1,800.00 POL", isFaded = false),
|
||||
)
|
||||
// Unsettled swap — the "You receive" side is struck through until the funds arrive.
|
||||
TxHistoryDetailsTwoAssetsBlock(
|
||||
from = previewAsset(label = "You sent", amount = "- 390 USDT", isFaded = false),
|
||||
to = previewAsset(label = "You receive", amount = "1,800.00 POL", isFaded = true),
|
||||
)
|
||||
// Account -> another account (own-to-own transfer between two of the user's accounts).
|
||||
TxHistoryDetailsTwoAssetsBlock(
|
||||
from = previewAsset(
|
||||
label = "From",
|
||||
amount = "- 390 USDT",
|
||||
isFaded = false,
|
||||
owner = AssetOwnerUM.Account(
|
||||
name = stringReference("Main account"),
|
||||
iconResId = R.drawable.ic_rounded_star_24,
|
||||
backgroundColor = Color(0xFF007FFF),
|
||||
),
|
||||
),
|
||||
to = previewAsset(
|
||||
label = "To",
|
||||
amount = "+ 1,800.00 POL",
|
||||
isFaded = false,
|
||||
owner = AssetOwnerUM.Account(
|
||||
name = stringReference("Family"),
|
||||
iconResId = R.drawable.ic_family_24,
|
||||
backgroundColor = Color(0xFF744FF1),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Wallet -> another wallet (own-to-own transfer between two of the user's wallets).
|
||||
TxHistoryDetailsTwoAssetsBlock(
|
||||
from = previewAsset(
|
||||
label = "From",
|
||||
amount = "- 390 USDT",
|
||||
isFaded = false,
|
||||
owner = AssetOwnerUM.Wallet(
|
||||
name = stringReference("Tangem 2.0"),
|
||||
deviceIconUM = DeviceIconUM.Card(mainColor = Color(0xFF1E1E1E), secondColor = null),
|
||||
),
|
||||
),
|
||||
to = previewAsset(
|
||||
label = "To",
|
||||
amount = "+ 1,800.00 POL",
|
||||
isFaded = false,
|
||||
owner = AssetOwnerUM.Wallet(
|
||||
name = stringReference("My Wallet"),
|
||||
deviceIconUM = DeviceIconUM.Ring(mainColor = Color(0xFF9F86FF)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun previewAsset(label: String, amount: String, isFaded: Boolean, owner: AssetOwnerUM? = null) = AssetUM(
|
||||
label = stringReference(label),
|
||||
owner = owner,
|
||||
amount = stringReference(amount),
|
||||
currencyIcon = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.img_eth_22,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
isFaded = isFaded,
|
||||
)
|
||||
|
||||
// endregion
|
||||
|
|
@ -102,6 +102,61 @@ internal class TxInfoToTxHistoryDetailsUMConverterTest {
|
|||
assertThat(header.iconRes).isEqualTo(R.drawable.ic_exchange_vertical_24)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unconfirmed Swap WHEN convert THEN info status banner with loader`() {
|
||||
// Arrange
|
||||
val tx = txInfo(type = TransactionType.Swap, status = TxInfo.TransactionStatus.Unconfirmed)
|
||||
|
||||
// Act
|
||||
val banner = (converter.convert(tx) as TxHistoryDetailsUM.TwoAssets).statusBanner
|
||||
|
||||
// Assert
|
||||
assertThat(banner).isEqualTo(
|
||||
TxHistoryDetailsUM.StatusBannerUM(
|
||||
severity = TxHistoryDetailsUM.StatusBannerUM.Severity.Info,
|
||||
title = resourceReference(R.string.express_exchange_status_receiving_active),
|
||||
isLoading = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN confirmed Swap WHEN convert THEN success status banner without loader`() {
|
||||
// Arrange
|
||||
val tx = txInfo(type = TransactionType.Swap, status = TxInfo.TransactionStatus.Confirmed)
|
||||
|
||||
// Act
|
||||
val banner = (converter.convert(tx) as TxHistoryDetailsUM.TwoAssets).statusBanner
|
||||
|
||||
// Assert
|
||||
assertThat(banner).isEqualTo(
|
||||
TxHistoryDetailsUM.StatusBannerUM(
|
||||
severity = TxHistoryDetailsUM.StatusBannerUM.Severity.Success,
|
||||
title = resourceReference(R.string.express_exchange_status_exchanged),
|
||||
isLoading = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN failed Swap WHEN convert THEN error status banner with refund subtitle`() {
|
||||
// Arrange
|
||||
val tx = txInfo(type = TransactionType.Swap, status = TxInfo.TransactionStatus.Failed)
|
||||
|
||||
// Act
|
||||
val banner = (converter.convert(tx) as TxHistoryDetailsUM.TwoAssets).statusBanner
|
||||
|
||||
// Assert
|
||||
assertThat(banner).isEqualTo(
|
||||
TxHistoryDetailsUM.StatusBannerUM(
|
||||
severity = TxHistoryDetailsUM.StatusBannerUM.Severity.Error,
|
||||
title = resourceReference(R.string.express_exchange_status_failed),
|
||||
subtitle = resourceReference(R.string.express_exchange_notification_failed_text),
|
||||
isLoading = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN incoming Transfer WHEN convert THEN amount block has plus sign and not failed`() {
|
||||
// Arrange
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue