Updated on 2026-08-14
This commit is contained in:
parent
e95243ab79
commit
e70f8be7c8
14 changed files with 392 additions and 357 deletions
|
|
@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
|
|
@ -32,6 +33,8 @@ import com.tangem.features.tokendetails.ExpressTransactionsComponent
|
|||
import com.tangem.features.tokendetails.TokenDetailsComponent
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import com.tangem.features.txhistory.component.TxHistoryComponent
|
||||
import com.tangem.features.txhistory.component.TxHistoryDetailsComponent
|
||||
import com.tangem.features.txhistory.component.TxHistoryDetailsSlotConfig
|
||||
import com.tangem.features.yield.supply.api.YieldSupplyComponent
|
||||
import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -44,6 +47,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
@Assisted params: TokenDetailsComponent.Params,
|
||||
tokenMarketBlockComponentFactory: TokenMarketBlockComponent.Factory,
|
||||
txHistoryComponentFactory: TxHistoryComponent.Factory,
|
||||
private val txHistoryDetailsComponentFactory: TxHistoryDetailsComponent.Factory,
|
||||
expressTransactionsComponentFactory: ExpressTransactionsComponent.Factory,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
private val yieldSupplyWarningComponentFactory: YieldSupplyDepositedWarningComponent.Factory,
|
||||
|
|
@ -59,6 +63,9 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
userWalletId = params.userWalletId,
|
||||
currency = params.currency,
|
||||
openExplorer = model::onExploreClick,
|
||||
onTxDetailsRequested = { txHistoryInfo ->
|
||||
model.txDetailsNavigation.activate(TxHistoryDetailsSlotConfig(txHistoryInfo))
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -88,6 +95,24 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
},
|
||||
)
|
||||
|
||||
private val txHistoryDetailsSlot = childSlot(
|
||||
key = TX_HISTORY_DETAILS_SLOT_KEY,
|
||||
source = model.txDetailsNavigation,
|
||||
serializer = null,
|
||||
handleBackButton = true,
|
||||
childFactory = { config, ctx ->
|
||||
txHistoryDetailsComponentFactory.create(
|
||||
context = childByContext(ctx),
|
||||
params = TxHistoryDetailsComponent.Params(
|
||||
txHistoryInfo = config.txHistoryInfo,
|
||||
userWalletId = params.userWalletId,
|
||||
currency = params.currency,
|
||||
onDismiss = model.txDetailsNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
private val tokenMarketBlockComponent = params.currency.toTokenMarketParam()?.let { tokenMarketParams ->
|
||||
tokenMarketBlockComponentFactory.create(
|
||||
appComponentContext = child("tokenMarketBlockComponent"),
|
||||
|
|
@ -109,6 +134,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
override fun Content(modifier: Modifier) {
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
val ratingSlotState by ratingSlot.subscribeAsState()
|
||||
val txHistoryDetails by txHistoryDetailsSlot.subscribeAsState()
|
||||
NavigationBar3ButtonsScrim()
|
||||
|
||||
if (LocalRedesignEnabled.current) {
|
||||
|
|
@ -136,6 +162,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
txHistoryDetails.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.toTokenMarketParam(): TokenMarketBlockComponent.Params? {
|
||||
|
|
@ -204,5 +231,6 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
|
||||
companion object {
|
||||
private const val RATING_SLOT_KEY = "ratingSlot"
|
||||
private const val TX_HISTORY_DETAILS_SLOT_KEY = "txHistoryDetailsSlot"
|
||||
}
|
||||
}
|
||||
|
|
@ -128,6 +128,7 @@ import com.tangem.features.tokendetails.ExpressTransactionsEvent
|
|||
import com.tangem.features.tokendetails.ExpressTransactionsEventListener
|
||||
import com.tangem.features.tokendetails.TokenDetailsComponent
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.features.txhistory.component.TxHistoryDetailsSlotConfig
|
||||
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
|
||||
import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent
|
||||
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
|
||||
|
|
@ -232,6 +233,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
|
||||
val bottomSheetNavigation: SlotNavigation<TokenDetailsBottomSheetConfig> = SlotNavigation()
|
||||
val ratingSlotNavigation = SlotNavigation<RatingComponent.Params>()
|
||||
val txDetailsNavigation = SlotNavigation<TxHistoryDetailsSlotConfig>()
|
||||
|
||||
private val stateFactory = TokenDetailsStateFactory(
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import androidx.compose.runtime.Stable
|
|||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.txhistory.model.TxHistoryInfo
|
||||
import com.tangem.features.txhistory.entity.TxHistoryItemsUM
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Stable
|
||||
|
|
@ -25,6 +27,7 @@ interface TxHistoryComponent {
|
|||
val userWalletId: UserWalletId,
|
||||
val currency: CryptoCurrency,
|
||||
val openExplorer: () -> Unit,
|
||||
val onTxDetailsRequested: (Flow<TxHistoryInfo>) -> Unit,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, TxHistoryComponent>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ package com.tangem.features.txhistory.component
|
|||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.txhistory.model.TxHistoryInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TxHistoryDetailsComponent : ComposableBottomSheetComponent {
|
||||
|
||||
data class Params(
|
||||
val txInfo: Flow<TxInfo>,
|
||||
val txHistoryInfo: Flow<TxHistoryInfo>,
|
||||
val userWalletId: UserWalletId,
|
||||
val currency: CryptoCurrency,
|
||||
val onDismiss: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -12,10 +12,9 @@ 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.utils.toTimeFormat
|
||||
import com.tangem.domain.express.models.ExpressExchangeStatus
|
||||
import com.tangem.domain.express.models.ExpressOnrampStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.txhistory.model.ExpressTx
|
||||
import com.tangem.domain.txhistory.model.explorerHash
|
||||
import com.tangem.features.txhistory.impl.R
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
import com.tangem.utils.StringsSigns
|
||||
|
|
@ -30,8 +29,8 @@ import java.math.BigDecimal
|
|||
* express statuses collapse into the three [Status] buckets (those drive title/icon/amount colors in the row UI).
|
||||
*
|
||||
* The counterparty ticker symbol+icon come from the resolved [ExpressTransactionAsset.cryptoCurrency] (swap);
|
||||
* onramp shows the real fiat code with no icon yet (fiat carries no `CryptoCurrency`). The row click opens the
|
||||
* explorer.
|
||||
* onramp shows the real fiat code with no icon yet (fiat carries no `CryptoCurrency`). The row click routes through
|
||||
* [TxHistoryUiActions.onTransactionClick] (express rows open the in-app details sheet).
|
||||
*/
|
||||
internal class ExpressTxToTransactionItemUMConverter(
|
||||
private val currency: CryptoCurrency,
|
||||
|
|
@ -39,6 +38,8 @@ internal class ExpressTxToTransactionItemUMConverter(
|
|||
) : Converter<ExpressTx, TransactionItemUM> {
|
||||
|
||||
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
|
||||
private val exchangeStatusConverter = ExpressExchangeStatusToUiStatusConverter()
|
||||
private val onrampStatusConverter = ExpressOnrampStatusToUiStatusConverter()
|
||||
|
||||
override fun convert(value: ExpressTx): TransactionItemUM = when (value) {
|
||||
is ExpressTx.Swap -> swapContent(value)
|
||||
|
|
@ -46,7 +47,7 @@ internal class ExpressTxToTransactionItemUMConverter(
|
|||
}
|
||||
|
||||
private fun swapContent(swap: ExpressTx.Swap): TransactionItemUM.Content {
|
||||
val status = swap.tx.status.toUiStatus()
|
||||
val status = exchangeStatusConverter.convert(swap.tx.status)
|
||||
val viewedAmount = if (swap.isOutgoing) swap.tx.fromAsset.amount else swap.tx.toAsset.amount
|
||||
val counterparty = if (swap.isOutgoing) swap.tx.toAsset else swap.tx.fromAsset
|
||||
val prefix = when {
|
||||
|
|
@ -72,7 +73,7 @@ internal class ExpressTxToTransactionItemUMConverter(
|
|||
}
|
||||
|
||||
private fun onrampContent(onramp: ExpressTx.Onramp): TransactionItemUM.Content {
|
||||
val status = onramp.tx.status.toUiStatus()
|
||||
val status = onrampStatusConverter.convert(onramp.tx.status)
|
||||
val prefix = when {
|
||||
status is Status.Failed -> ""
|
||||
status is Status.Confirmed -> StringsSigns.PLUS
|
||||
|
|
@ -107,15 +108,15 @@ internal class ExpressTxToTransactionItemUMConverter(
|
|||
subtitle: ContentSubtitle,
|
||||
warning: TextReference?,
|
||||
): TransactionItemUM.Content {
|
||||
val explorerHash = tx.matchHash ?: tx.txId
|
||||
// Row identity (Compose key): the on-chain leg's hash when matched, else the express txId stands in.
|
||||
return TransactionItemUM.Content(
|
||||
txHash = explorerHash,
|
||||
txHash = tx.explorerHash ?: tx.txId,
|
||||
amount = amount,
|
||||
currencySymbol = currency.symbol,
|
||||
time = tx.timestampMillis.toTimeFormat(),
|
||||
status = status,
|
||||
direction = direction,
|
||||
onClick = { txHistoryUiActions.openTxInExplorer(explorerHash) },
|
||||
onClick = { txHistoryUiActions.onTransactionClick(tx) },
|
||||
iconRes = iconRes,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
|
|
@ -142,50 +143,4 @@ internal class ExpressTxToTransactionItemUMConverter(
|
|||
wrappedList(resourceReference(R.string.tx_history_onramp_top_up)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Status mapping
|
||||
|
||||
/**
|
||||
* Collapses the typed swap status into a UI [Status] bucket: the single success state ([Finished][Confirmed]),
|
||||
* the failure/return states ([Failed]/[TxFailed]/[Refunded]/[Expired]/[Unknown]) → Failed, everything in flight
|
||||
* (incl. [Verifying] and [Paused]) → Unconfirmed.
|
||||
*/
|
||||
private fun ExpressExchangeStatus.toUiStatus(): Status = when (this) {
|
||||
ExpressExchangeStatus.Finished -> Status.Confirmed
|
||||
ExpressExchangeStatus.Failed,
|
||||
ExpressExchangeStatus.TxFailed,
|
||||
ExpressExchangeStatus.Refunded,
|
||||
ExpressExchangeStatus.Expired,
|
||||
ExpressExchangeStatus.Unknown,
|
||||
-> Status.Failed
|
||||
ExpressExchangeStatus.Preview,
|
||||
ExpressExchangeStatus.Created,
|
||||
ExpressExchangeStatus.ExchangeTxSent,
|
||||
ExpressExchangeStatus.Waiting,
|
||||
ExpressExchangeStatus.WaitingTxHash,
|
||||
ExpressExchangeStatus.Confirming,
|
||||
ExpressExchangeStatus.Exchanging,
|
||||
ExpressExchangeStatus.Sending,
|
||||
ExpressExchangeStatus.Verifying,
|
||||
ExpressExchangeStatus.Paused,
|
||||
-> Status.Unconfirmed
|
||||
}
|
||||
|
||||
private fun ExpressOnrampStatus.toUiStatus(): Status = when (this) {
|
||||
ExpressOnrampStatus.Finished -> Status.Confirmed
|
||||
ExpressOnrampStatus.Failed,
|
||||
ExpressOnrampStatus.Expired,
|
||||
ExpressOnrampStatus.Unknown,
|
||||
-> Status.Failed
|
||||
ExpressOnrampStatus.Created,
|
||||
ExpressOnrampStatus.WaitingForPayment,
|
||||
ExpressOnrampStatus.PaymentProcessing,
|
||||
ExpressOnrampStatus.Verifying,
|
||||
ExpressOnrampStatus.Paid,
|
||||
ExpressOnrampStatus.Sending,
|
||||
ExpressOnrampStatus.Paused,
|
||||
-> Status.Unconfirmed
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -4,15 +4,21 @@ import com.tangem.core.ui.components.transactions.state.TransactionItemUM
|
|||
import com.tangem.domain.txhistory.model.ExpressTx
|
||||
import com.tangem.domain.txhistory.model.OnChainTx
|
||||
import com.tangem.domain.txhistory.model.TxHistoryInfo
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converts a merged [TxHistoryInfo] row to [TransactionItemUM]: on-chain rows convert their `TxInfo` via
|
||||
* [TxHistoryItemToTransactionItemUMConverter]; express rows map directly via [ExpressTxToTransactionItemUMConverter].
|
||||
* Converts a merged [TxHistoryInfo] row to [TransactionItemUM].
|
||||
*
|
||||
* On-chain rows render via the plain [TxHistoryItemToTransactionItemUMConverter] (which knows only `TxInfo`); since
|
||||
* that converter cannot reference the merged row, the row click is bound here to the **incoming** [OnChainTx] so the
|
||||
* model resolves it in the live list by [TxHistoryInfo.txId]. Express rows carry their own [TxHistoryInfo] and wire
|
||||
* the click themselves in [ExpressTxToTransactionItemUMConverter].
|
||||
*/
|
||||
internal class TxHistoryInfoToTransactionItemUMConverter(
|
||||
private val txInfoConverter: TxHistoryItemToTransactionItemUMConverter,
|
||||
private val expressConverter: ExpressTxToTransactionItemUMConverter,
|
||||
private val txHistoryUiActions: TxHistoryUiActions,
|
||||
) : Converter<TxHistoryInfo, TransactionItemUM> {
|
||||
|
||||
override fun convert(value: TxHistoryInfo): TransactionItemUM = when (value) {
|
||||
|
|
@ -21,6 +27,10 @@ internal class TxHistoryInfoToTransactionItemUMConverter(
|
|||
}
|
||||
|
||||
private fun convertOnChain(value: OnChainTx): TransactionItemUM = when (value) {
|
||||
is OnChainTx.BSDK -> txInfoConverter.convert(value.txInfo)
|
||||
is OnChainTx.BSDK -> when (val um = txInfoConverter.convert(value.txInfo)) {
|
||||
// Content rows (transfer/swap/…) route through the details/explorer decision; pills stay on the explorer.
|
||||
is TransactionItemUM.Content -> um.copy(onClick = { txHistoryUiActions.onTransactionClick(value) })
|
||||
else -> um
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
package com.tangem.features.txhistory.converter
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM
|
||||
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.format
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
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
|
||||
|
||||
/**
|
||||
* Converts a [TxInfo] to a [TxHistoryDetailsUM] for the in-app transaction details card.
|
||||
*
|
||||
* Single dispatch on [TxInfo.type] picks the layout family — mirroring the same `when(type)` used by
|
||||
* [TxHistoryItemToTransactionItemUMConverter]:
|
||||
* - [TransactionType.Swap] (and onramp once it lands in `TxInfo`) -> [TxHistoryDetailsUM.TwoAssets]
|
||||
* - everything else -> [TxHistoryDetailsUM.SingleAsset]
|
||||
*/
|
||||
internal class TxInfoToTxHistoryDetailsUMConverter(
|
||||
private val currency: CryptoCurrency,
|
||||
private val onCopyAddress: (String) -> Unit,
|
||||
) : Converter<TxInfo, TxHistoryDetailsUM> {
|
||||
|
||||
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
|
||||
|
||||
override fun convert(value: TxInfo): TxHistoryDetailsUM = when (value.type) {
|
||||
// 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 = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun TxInfo.toHeaderUM(): TxHistoryDetailsUM.HeaderUM = TxHistoryDetailsUM.HeaderUM(
|
||||
iconRes = headerIcon(),
|
||||
status = status.toUiStatus(),
|
||||
title = headerTitle(),
|
||||
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)),
|
||||
// TODO: TxInfo has no fiat amount yet — empty until the fiat field is added to TxInfo; a hardcoded
|
||||
// placeholder would show a misleading value.
|
||||
fiatAmount = TextReference.EMPTY,
|
||||
isFailed = status is TxInfo.TransactionStatus.Failed,
|
||||
)
|
||||
|
||||
/**
|
||||
* Counterparty card ("Recipient" / "From"). Currently only the external-address avatar is produced — built from
|
||||
* the `User` interaction address (the same source the history list uses for its external-address subtitle).
|
||||
*
|
||||
* The own-account / own-wallet avatars ([TxHistoryDetailsUM.CounterpartyAvatar.Account] / `Wallet`) require the
|
||||
* address->owner lookup the list assembles in `TxHistoryLookupContext`; wiring that into the detail model is a
|
||||
* follow-up, so for now a counterparty that is not a plain external `User` address yields no card (`null`).
|
||||
*/
|
||||
private fun TxInfo.toCounterpartyUM(): TxHistoryDetailsUM.CounterpartyUM? {
|
||||
val address = (interactionAddressType as? TxInfo.InteractionAddressType.User)?.address ?: return null
|
||||
return TxHistoryDetailsUM.CounterpartyUM(
|
||||
label = counterpartyLabel(),
|
||||
title = stringReference(address.toBriefAddressFormat()),
|
||||
avatar = TxHistoryDetailsUM.CounterpartyAvatar.Address(rawAddress = address),
|
||||
onCopyClick = { onCopyAddress(address) },
|
||||
)
|
||||
}
|
||||
|
||||
/** Section label above the counterparty: "Recipient" for outgoing transfers, "From" for incoming. */
|
||||
private fun TxInfo.counterpartyLabel(): TextReference =
|
||||
if (isOutgoing) resourceReference(R.string.send_recipient) else resourceReference(R.string.common_from)
|
||||
}
|
||||
|
||||
// region Amount building helpers
|
||||
|
||||
/**
|
||||
* Signed crypto amount with inline symbol, e.g. `+ 350.31 USDT` / `- 350.31 USDT`. The sign is `-` for outgoing, `+`
|
||||
* otherwise, and is dropped for zero amounts and for the failed state (a failed tx moved nothing) — the UI then only
|
||||
* strikes the amount through and dims it via [TxHistoryDetailsUM.AmountBlockUM.isFailed].
|
||||
*/
|
||||
private fun TxInfo.signedAmount(currency: CryptoCurrency): String {
|
||||
val formatted = amount.format { crypto(cryptoCurrency = currency, ignoreSymbolPosition = true) }
|
||||
val prefix = when {
|
||||
status is TxInfo.TransactionStatus.Failed -> ""
|
||||
amount.isZero() -> ""
|
||||
isOutgoing -> "${StringsSigns.MINUS} "
|
||||
else -> "${StringsSigns.PLUS} "
|
||||
}
|
||||
return (prefix + formatted).trim()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// 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.headerTitle(): TextReference = when (type) {
|
||||
is TransactionType.Swap -> statusAwareTitle(R.string.common_swapping, R.string.common_swapped)
|
||||
is TransactionType.Transfer -> statusAwareTitle(R.string.common_transfer, R.string.common_transferred)
|
||||
else -> stringReference(type.toString())
|
||||
}
|
||||
|
||||
private fun TxInfo.headerSubtitle(): TextReference {
|
||||
val dateTime = DateTime(timestampInMillis)
|
||||
val date = DateTimeFormatters.dateMMMdYYYY.print(dateTime)
|
||||
val time = DateTimeFormatters.timeFormatter.print(dateTime)
|
||||
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(): 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
|
||||
|
|
@ -6,7 +6,7 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.features.txhistory.component.TxHistoryDetailsComponent
|
||||
import com.tangem.features.txhistory.converter.TxInfoToTxHistoryDetailsUMConverter
|
||||
import com.tangem.features.txhistory.converter.TxHistoryInfoToTxHistoryDetailsUMConverter
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
|
|
@ -26,12 +26,12 @@ internal class TxHistoryDetailsModel @Inject constructor(
|
|||
|
||||
private val params: TxHistoryDetailsComponent.Params = paramsContainer.require()
|
||||
|
||||
private val converter = TxInfoToTxHistoryDetailsUMConverter(
|
||||
private val converter = TxHistoryInfoToTxHistoryDetailsUMConverter(
|
||||
currency = params.currency,
|
||||
onCopyAddress = ::onCopyAddress,
|
||||
)
|
||||
|
||||
val uiState: StateFlow<TxHistoryDetailsUM?> = params.txInfo
|
||||
val uiState: StateFlow<TxHistoryDetailsUM?> = params.txHistoryInfo
|
||||
.map(converter::convert)
|
||||
.flowOn(dispatchers.default)
|
||||
.stateIn(modelScope, SharingStarted.WhileSubscribed(), initialValue = null)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.filterCryptoPortfolio
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.txhistory.model.ExpressTx
|
||||
import com.tangem.domain.txhistory.model.OnChainTx
|
||||
import com.tangem.domain.txhistory.model.TxHistoryInfo
|
||||
import com.tangem.domain.txhistory.model.explorerHash
|
||||
import com.tangem.domain.txhistory.models.TxHistoryStateError
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
|
|
@ -211,6 +215,7 @@ internal class TxHistoryModel @Inject constructor(
|
|||
currency = params.currency,
|
||||
txHistoryUiActions = this,
|
||||
),
|
||||
txHistoryUiActions = this,
|
||||
)
|
||||
|
||||
val items = mutableListOf<TxHistoryItemsUM.TxHistoryItemUM>()
|
||||
|
|
@ -361,4 +366,20 @@ internal class TxHistoryModel @Inject constructor(
|
|||
ifRight = { urlOpener.openUrl(url = it) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTransactionClick(item: TxHistoryInfo) {
|
||||
// manager is non-null only under the new tx-history toggle; on the legacy path every tap falls to the explorer.
|
||||
val manager = historyTxListManager
|
||||
if (manager != null && item.opensInAppDetails()) {
|
||||
params.onTxDetailsRequested(manager.txHistoryInfoFlow(item))
|
||||
} else {
|
||||
item.explorerHash?.let(::openTxInExplorer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** On-chain transfers/swaps and every express op open the in-app details sheet; everything else goes to the explorer. */
|
||||
private fun TxHistoryInfo.opensInAppDetails(): Boolean = when (this) {
|
||||
is ExpressTx -> true
|
||||
is OnChainTx.BSDK -> txInfo.type is TxInfo.TransactionType.Transfer || txInfo.type is TxInfo.TransactionType.Swap
|
||||
}
|
||||
|
|
@ -120,16 +120,6 @@ internal class TxHistoryListManager(
|
|||
)
|
||||
}
|
||||
|
||||
fun txInfoFlow(txHash: String, type: TxInfo.TransactionType): Flow<TxInfo> = state
|
||||
.map { st ->
|
||||
st.rawBatches.asSequence()
|
||||
.flatMap { it.data.items }
|
||||
.firstOrNull { it.txHash == txHash && it.type == type }
|
||||
}
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
.flowOn(dispatchers.default)
|
||||
|
||||
private fun updateState(
|
||||
batchListState: BatchListState<Int, PaginationWrapper<TxInfo>>,
|
||||
lookupContext: TxHistoryLookupContext?,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
import com.tangem.domain.txhistory.model.TxHistoryInfo
|
||||
|
||||
internal interface TxHistoryUiActions {
|
||||
|
||||
fun openExplorer()
|
||||
fun openTxInExplorer(txHash: String)
|
||||
fun onTransactionClick(item: TxHistoryInfo)
|
||||
}
|
||||
|
|
@ -196,21 +196,19 @@ internal class ExpressTxToTransactionItemUMConverterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN matched on-chain leg WHEN row clicked THEN opens explorer by match hash`() {
|
||||
val result = converter.convert(
|
||||
createSwap(status = ExpressExchangeStatus.Waiting, matchHash = "0xhash", isOutgoing = true),
|
||||
) as TransactionItemUM.Content
|
||||
fun `GIVEN express row WHEN row clicked THEN opens in-app details for that tx`() {
|
||||
val swap = createSwap(status = ExpressExchangeStatus.Waiting, isOutgoing = true)
|
||||
val result = converter.convert(swap) as TransactionItemUM.Content
|
||||
|
||||
result.onClick()
|
||||
|
||||
verify { txHistoryUiActions.openTxInExplorer("0xhash") }
|
||||
verify { txHistoryUiActions.onTransactionClick(swap) }
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private fun createSwap(
|
||||
status: ExpressExchangeStatus,
|
||||
matchHash: String? = null,
|
||||
isOutgoing: Boolean = true,
|
||||
fromAmount: BigDecimal? = BigDecimal("1.5"),
|
||||
toAmount: BigDecimal? = BigDecimal("0.001"),
|
||||
|
|
@ -220,8 +218,8 @@ internal class ExpressTxToTransactionItemUMConverterTest {
|
|||
status = status,
|
||||
createdAtMillis = 100,
|
||||
provider = null,
|
||||
payinHash = matchHash.takeIf { isOutgoing },
|
||||
payoutHash = matchHash.takeUnless { isOutgoing },
|
||||
payinHash = null,
|
||||
payoutHash = null,
|
||||
fromAsset = ExpressTransactionAsset(
|
||||
id = ExpressAssetId(networkId = "eth", contractAddress = "0"),
|
||||
amount = fromAmount,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
package com.tangem.features.txhistory.converter
|
||||
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM
|
||||
import com.tangem.domain.express.models.ExchangeTransaction
|
||||
import com.tangem.domain.express.models.ExpressAsset.ID as ExpressAssetId
|
||||
import com.tangem.domain.express.models.ExpressExchangeStatus
|
||||
import com.tangem.domain.express.models.ExpressTransactionAsset
|
||||
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.txhistory.model.ExpressTx
|
||||
import com.tangem.domain.txhistory.model.OnChainTx
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TxHistoryInfoToTransactionItemUMConverterTest {
|
||||
|
||||
private val txHistoryUiActions: TxHistoryUiActions = mockk(relaxed = true)
|
||||
private val currency: CryptoCurrency = MockCryptoCurrencyFactory().ethereum
|
||||
|
||||
private val converter = TxHistoryInfoToTransactionItemUMConverter(
|
||||
txInfoConverter = TxHistoryItemToTransactionItemUMConverter(
|
||||
currency = currency,
|
||||
txHistoryUiActions = txHistoryUiActions,
|
||||
),
|
||||
expressConverter = ExpressTxToTransactionItemUMConverter(
|
||||
currency = currency,
|
||||
txHistoryUiActions = txHistoryUiActions,
|
||||
),
|
||||
txHistoryUiActions = txHistoryUiActions,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN on-chain content row WHEN row clicked THEN routes the incoming OnChainTx through onTransactionClick`() {
|
||||
// Arrange
|
||||
val item = OnChainTx.BSDK(txInfo(type = TransactionType.Transfer))
|
||||
|
||||
// Act
|
||||
val result = converter.convert(item) as TransactionItemUM.Content
|
||||
result.onClick()
|
||||
|
||||
// Assert
|
||||
verify { txHistoryUiActions.onTransactionClick(item) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN on-chain swap row WHEN row clicked THEN routes the incoming OnChainTx through onTransactionClick`() {
|
||||
// Arrange
|
||||
val item = OnChainTx.BSDK(txInfo(type = TransactionType.Swap))
|
||||
|
||||
// Act
|
||||
val result = converter.convert(item) as TransactionItemUM.Content
|
||||
result.onClick()
|
||||
|
||||
// Assert
|
||||
verify { txHistoryUiActions.onTransactionClick(item) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN on-chain pill row WHEN row clicked THEN stays on the explorer`() {
|
||||
// Arrange
|
||||
val item = OnChainTx.BSDK(txInfo(type = TransactionType.Approve))
|
||||
|
||||
// Act
|
||||
val result = converter.convert(item) as TransactionItemUM.Pill
|
||||
result.onClick()
|
||||
|
||||
// Assert
|
||||
verify { txHistoryUiActions.openTxInExplorer(TX_HASH) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN express row WHEN row clicked THEN routes the incoming ExpressTx through onTransactionClick`() {
|
||||
// Arrange
|
||||
val item = expressSwap()
|
||||
|
||||
// Act
|
||||
val result = converter.convert(item) as TransactionItemUM.Content
|
||||
result.onClick()
|
||||
|
||||
// Assert
|
||||
verify { txHistoryUiActions.onTransactionClick(item) }
|
||||
}
|
||||
|
||||
private fun txInfo(type: TransactionType): TxInfo = TxInfo(
|
||||
txHash = TX_HASH,
|
||||
timestampInMillis = TIMESTAMP,
|
||||
isOutgoing = false,
|
||||
destinationType = TxInfo.DestinationType.Single(addressType = TxInfo.AddressType.User(USER_ADDRESS)),
|
||||
sourceType = TxInfo.SourceType.Single(address = USER_ADDRESS),
|
||||
interactionAddressType = TxInfo.InteractionAddressType.User(USER_ADDRESS),
|
||||
status = TxInfo.TransactionStatus.Confirmed,
|
||||
type = type,
|
||||
amount = BigDecimal.ONE,
|
||||
)
|
||||
|
||||
private fun expressSwap(): ExpressTx.Swap = ExpressTx.Swap(
|
||||
tx = ExchangeTransaction(
|
||||
txId = "swap-1",
|
||||
status = ExpressExchangeStatus.Exchanging,
|
||||
createdAtMillis = TIMESTAMP,
|
||||
provider = null,
|
||||
payinHash = null,
|
||||
payoutHash = null,
|
||||
fromAsset = ExpressTransactionAsset(
|
||||
id = ExpressAssetId(networkId = "ethereum", contractAddress = "0"),
|
||||
amount = BigDecimal("1.5"),
|
||||
decimals = 18,
|
||||
),
|
||||
toAsset = ExpressTransactionAsset(
|
||||
id = ExpressAssetId(networkId = "bitcoin", contractAddress = "0"),
|
||||
amount = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
),
|
||||
isOutgoing = true,
|
||||
txInfo = null,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val TX_HASH = "0xtxhash"
|
||||
const val TIMESTAMP = 1_700_000_000_000L
|
||||
const val USER_ADDRESS = "0x1234567890abcdef1234"
|
||||
}
|
||||
}
|
||||
|
|
@ -6,10 +6,21 @@ import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
|||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.express.models.ExchangeTransaction
|
||||
import com.tangem.domain.express.models.ExpressAsset.ID as ExpressAssetId
|
||||
import com.tangem.domain.express.models.ExpressExchangeStatus
|
||||
import com.tangem.domain.express.models.ExpressOnrampStatus
|
||||
import com.tangem.domain.express.models.ExpressTransactionAsset
|
||||
import com.tangem.domain.express.models.OnrampTransaction
|
||||
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.features.txhistory.entity.TxHistoryDetailsUM
|
||||
import com.tangem.features.txhistory.impl.R
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
|
|
@ -17,14 +28,15 @@ import org.junit.jupiter.api.AfterEach
|
|||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TxInfoToTxHistoryDetailsUMConverterTest {
|
||||
internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
|
||||
|
||||
private val currency = MockCryptoCurrencyFactory().ethereum
|
||||
private val copiedAddresses = mutableListOf<String>()
|
||||
private val converter = TxInfoToTxHistoryDetailsUMConverter(
|
||||
private val converter = TxHistoryInfoToTxHistoryDetailsUMConverter(
|
||||
currency = currency,
|
||||
onCopyAddress = copiedAddresses::add,
|
||||
)
|
||||
|
|
@ -42,44 +54,49 @@ internal class TxInfoToTxHistoryDetailsUMConverterTest {
|
|||
unmockkStatic(DateFormat::class)
|
||||
}
|
||||
|
||||
// region On-chain (TxInfo)
|
||||
|
||||
@Test
|
||||
fun `GIVEN Swap WHEN convert THEN TwoAssets`() {
|
||||
fun `GIVEN on-chain Swap WHEN convert THEN SingleAsset fallback`() {
|
||||
// A two-asset swap always surfaces as ExpressTx.Swap; an on-chain TxInfo of type Swap has no legs,
|
||||
// so it falls back to the single amount it does carry rather than an empty two-asset card.
|
||||
// Arrange
|
||||
val tx = txInfo(type = TransactionType.Swap)
|
||||
val tx = onChain(type = TransactionType.Swap)
|
||||
|
||||
// Act
|
||||
val result = converter.convert(tx)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isInstanceOf(TxHistoryDetailsUM.TwoAssets::class.java)
|
||||
assertThat(result).isInstanceOf(TxHistoryDetailsUM.SingleAsset::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-Swap TransactionType WHEN convert THEN SingleAsset`() {
|
||||
val nonSwapTypes = listOf(
|
||||
TransactionType.Transfer,
|
||||
TransactionType.Approve,
|
||||
TransactionType.Operation(name = "Mint NFT"),
|
||||
TransactionType.UnknownOperation,
|
||||
TransactionType.GaslessFee,
|
||||
TransactionType.Staking.Stake,
|
||||
TransactionType.Staking.ClaimRewards,
|
||||
TransactionType.Staking.Vote(validatorAddress = VALIDATOR_ADDRESS),
|
||||
TransactionType.YieldSupply.Topup,
|
||||
TransactionType.YieldSupply.Enter(address = USER_ADDRESS),
|
||||
)
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN non-Swap TransactionType WHEN convert THEN SingleAsset`(type: TransactionType) {
|
||||
// Act
|
||||
val result = converter.convert(onChain(type = type))
|
||||
|
||||
nonSwapTypes.forEach { type ->
|
||||
val result = converter.convert(txInfo(type = type))
|
||||
|
||||
assertThat(result).isInstanceOf(TxHistoryDetailsUM.SingleAsset::class.java)
|
||||
}
|
||||
// Assert
|
||||
assertThat(result).isInstanceOf(TxHistoryDetailsUM.SingleAsset::class.java)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
TransactionType.Transfer,
|
||||
TransactionType.Approve,
|
||||
TransactionType.Operation(name = "Mint NFT"),
|
||||
TransactionType.UnknownOperation,
|
||||
TransactionType.GaslessFee,
|
||||
TransactionType.Staking.Stake,
|
||||
TransactionType.Staking.ClaimRewards,
|
||||
TransactionType.Staking.Vote(validatorAddress = VALIDATOR_ADDRESS),
|
||||
TransactionType.YieldSupply.Topup,
|
||||
TransactionType.YieldSupply.Enter(address = USER_ADDRESS),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN incoming confirmed Transfer WHEN convert THEN header has down icon, confirmed status, transferred title`() {
|
||||
// Arrange
|
||||
val tx = txInfo(type = TransactionType.Transfer)
|
||||
val tx = onChain(type = TransactionType.Transfer)
|
||||
|
||||
// Act
|
||||
val header = converter.convert(tx).header
|
||||
|
|
@ -93,7 +110,7 @@ internal class TxInfoToTxHistoryDetailsUMConverterTest {
|
|||
@Test
|
||||
fun `GIVEN Swap WHEN convert THEN header has exchange icon`() {
|
||||
// Arrange
|
||||
val tx = txInfo(type = TransactionType.Swap)
|
||||
val tx = onChain(type = TransactionType.Swap)
|
||||
|
||||
// Act
|
||||
val header = converter.convert(tx).header
|
||||
|
|
@ -102,65 +119,10 @@ 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
|
||||
val tx = txInfo(type = TransactionType.Transfer, isOutgoing = false)
|
||||
val tx = onChain(type = TransactionType.Transfer, isOutgoing = false)
|
||||
|
||||
// Act
|
||||
val amountBlock = (converter.convert(tx) as TxHistoryDetailsUM.SingleAsset).amountBlock
|
||||
|
|
@ -173,7 +135,7 @@ internal class TxInfoToTxHistoryDetailsUMConverterTest {
|
|||
@Test
|
||||
fun `GIVEN outgoing Transfer WHEN convert THEN amount block has minus sign`() {
|
||||
// Arrange
|
||||
val tx = txInfo(type = TransactionType.Transfer, isOutgoing = true)
|
||||
val tx = onChain(type = TransactionType.Transfer, isOutgoing = true)
|
||||
|
||||
// Act
|
||||
val amountBlock = (converter.convert(tx) as TxHistoryDetailsUM.SingleAsset).amountBlock
|
||||
|
|
@ -185,7 +147,7 @@ internal class TxInfoToTxHistoryDetailsUMConverterTest {
|
|||
@Test
|
||||
fun `GIVEN zero amount WHEN convert THEN amount block has no sign`() {
|
||||
// Arrange
|
||||
val tx = txInfo(type = TransactionType.Transfer, isOutgoing = true, amount = BigDecimal.ZERO)
|
||||
val tx = onChain(type = TransactionType.Transfer, isOutgoing = true, amount = BigDecimal.ZERO)
|
||||
|
||||
// Act
|
||||
val amountBlock = (converter.convert(tx) as TxHistoryDetailsUM.SingleAsset).amountBlock
|
||||
|
|
@ -199,7 +161,7 @@ internal class TxInfoToTxHistoryDetailsUMConverterTest {
|
|||
@Test
|
||||
fun `GIVEN failed outgoing Transfer WHEN convert THEN amount block is failed and drops the sign`() {
|
||||
// Arrange
|
||||
val tx = txInfo(
|
||||
val tx = onChain(
|
||||
type = TransactionType.Transfer,
|
||||
isOutgoing = true,
|
||||
status = TxInfo.TransactionStatus.Failed,
|
||||
|
|
@ -218,7 +180,7 @@ internal class TxInfoToTxHistoryDetailsUMConverterTest {
|
|||
@Test
|
||||
fun `GIVEN no interaction address WHEN convert THEN counterparty is null`() {
|
||||
// Arrange
|
||||
val tx = txInfo(type = TransactionType.Transfer, interactionAddressType = null)
|
||||
val tx = onChain(type = TransactionType.Transfer, interactionAddressType = null)
|
||||
|
||||
// Act
|
||||
val counterparty = (converter.convert(tx) as TxHistoryDetailsUM.SingleAsset).counterparty
|
||||
|
|
@ -230,7 +192,7 @@ internal class TxInfoToTxHistoryDetailsUMConverterTest {
|
|||
@Test
|
||||
fun `GIVEN incoming Transfer with User address WHEN convert THEN address-avatar counterparty with From label`() {
|
||||
// Arrange
|
||||
val tx = txInfo(
|
||||
val tx = onChain(
|
||||
type = TransactionType.Transfer,
|
||||
isOutgoing = false,
|
||||
interactionAddressType = TxInfo.InteractionAddressType.User(USER_ADDRESS),
|
||||
|
|
@ -247,7 +209,7 @@ internal class TxInfoToTxHistoryDetailsUMConverterTest {
|
|||
@Test
|
||||
fun `GIVEN outgoing Transfer with User address WHEN convert THEN counterparty has Recipient label`() {
|
||||
// Arrange
|
||||
val tx = txInfo(
|
||||
val tx = onChain(
|
||||
type = TransactionType.Transfer,
|
||||
isOutgoing = true,
|
||||
interactionAddressType = TxInfo.InteractionAddressType.User(USER_ADDRESS),
|
||||
|
|
@ -263,7 +225,7 @@ internal class TxInfoToTxHistoryDetailsUMConverterTest {
|
|||
@Test
|
||||
fun `GIVEN address counterparty WHEN onCopyClick invoked THEN raw address is copied`() {
|
||||
// Arrange
|
||||
val tx = txInfo(
|
||||
val tx = onChain(
|
||||
type = TransactionType.Transfer,
|
||||
interactionAddressType = TxInfo.InteractionAddressType.User(USER_ADDRESS),
|
||||
)
|
||||
|
|
@ -276,24 +238,131 @@ internal class TxInfoToTxHistoryDetailsUMConverterTest {
|
|||
assertThat(copiedAddresses).containsExactly(USER_ADDRESS)
|
||||
}
|
||||
|
||||
private fun txInfo(
|
||||
// endregion
|
||||
|
||||
// region Express (swap / onramp)
|
||||
|
||||
@Test
|
||||
fun `GIVEN express swap WHEN convert THEN TwoAssets with exchange icon`() {
|
||||
// Act
|
||||
val result = converter.convert(expressSwap(status = ExpressExchangeStatus.Exchanging))
|
||||
|
||||
// Assert
|
||||
assertThat(result).isInstanceOf(TxHistoryDetailsUM.TwoAssets::class.java)
|
||||
assertThat(result.header.iconRes).isEqualTo(R.drawable.ic_exchange_vertical_24)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN in-progress express swap WHEN convert THEN info status banner with loader`() {
|
||||
// Act
|
||||
val swap = converter.convert(expressSwap(status = ExpressExchangeStatus.Exchanging))
|
||||
val banner = (swap 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 failed express swap WHEN convert THEN error status banner with refund subtitle`() {
|
||||
// Act
|
||||
val swap = converter.convert(expressSwap(status = ExpressExchangeStatus.Failed))
|
||||
val banner = (swap 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 finished express onramp WHEN convert THEN TwoAssets with success banner`() {
|
||||
// Act
|
||||
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.statusBanner).isEqualTo(
|
||||
TxHistoryDetailsUM.StatusBannerUM(
|
||||
severity = TxHistoryDetailsUM.StatusBannerUM.Severity.Success,
|
||||
title = resourceReference(R.string.express_exchange_status_exchanged),
|
||||
isLoading = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private fun onChain(
|
||||
type: TransactionType,
|
||||
isOutgoing: Boolean = false,
|
||||
status: TxInfo.TransactionStatus = TxInfo.TransactionStatus.Confirmed,
|
||||
amount: BigDecimal = BigDecimal.ONE,
|
||||
interactionAddressType: TxInfo.InteractionAddressType? = null,
|
||||
): TxInfo = TxInfo(
|
||||
txHash = TX_HASH,
|
||||
timestampInMillis = TIMESTAMP,
|
||||
isOutgoing = isOutgoing,
|
||||
destinationType = TxInfo.DestinationType.Single(addressType = TxInfo.AddressType.User(USER_ADDRESS)),
|
||||
sourceType = TxInfo.SourceType.Single(address = USER_ADDRESS),
|
||||
interactionAddressType = interactionAddressType,
|
||||
status = status,
|
||||
type = type,
|
||||
amount = amount,
|
||||
): OnChainTx.BSDK = OnChainTx.BSDK(
|
||||
TxInfo(
|
||||
txHash = TX_HASH,
|
||||
timestampInMillis = TIMESTAMP,
|
||||
isOutgoing = isOutgoing,
|
||||
destinationType = TxInfo.DestinationType.Single(addressType = TxInfo.AddressType.User(USER_ADDRESS)),
|
||||
sourceType = TxInfo.SourceType.Single(address = USER_ADDRESS),
|
||||
interactionAddressType = interactionAddressType,
|
||||
status = status,
|
||||
type = type,
|
||||
amount = amount,
|
||||
),
|
||||
)
|
||||
|
||||
private fun expressSwap(status: ExpressExchangeStatus): ExpressTx.Swap = ExpressTx.Swap(
|
||||
tx = ExchangeTransaction(
|
||||
txId = "swap-1",
|
||||
status = status,
|
||||
createdAtMillis = TIMESTAMP,
|
||||
provider = null,
|
||||
payinHash = null,
|
||||
payoutHash = null,
|
||||
fromAsset = expressAsset(networkId = "ethereum", amount = BigDecimal("1.5"), decimals = 18),
|
||||
toAsset = expressAsset(networkId = "bitcoin", amount = BigDecimal("0.001"), decimals = 8),
|
||||
),
|
||||
isOutgoing = true,
|
||||
txInfo = null,
|
||||
)
|
||||
|
||||
private fun expressOnramp(status: ExpressOnrampStatus): ExpressTx.Onramp = ExpressTx.Onramp(
|
||||
tx = OnrampTransaction(
|
||||
txId = "onramp-1",
|
||||
status = status,
|
||||
createdAtMillis = TIMESTAMP,
|
||||
provider = null,
|
||||
payoutHash = null,
|
||||
fromFiat = Amount(
|
||||
currencySymbol = "SEK",
|
||||
value = BigDecimal("100"),
|
||||
decimals = 2,
|
||||
type = AmountType.FiatType(code = "SEK"),
|
||||
),
|
||||
toAsset = expressAsset(networkId = "bitcoin", amount = BigDecimal("0.006"), decimals = 8),
|
||||
),
|
||||
txInfo = null,
|
||||
)
|
||||
|
||||
private fun expressAsset(networkId: String, amount: BigDecimal, decimals: Int): ExpressTransactionAsset =
|
||||
ExpressTransactionAsset(
|
||||
id = ExpressAssetId(networkId = networkId, contractAddress = "0"),
|
||||
amount = amount,
|
||||
decimals = decimals,
|
||||
)
|
||||
|
||||
private fun TextReference.resolveString(): String = (this as TextReference.Str).value
|
||||
|
||||
private companion object {
|
||||
Loading…
Add table
Add a link
Reference in a new issue