Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-25 18:19:23 +05:00
parent e72c2c87bc
commit 5e78d82235
31 changed files with 976 additions and 123 deletions

View file

@ -31,6 +31,13 @@ internal fun ContentIcon(
alpha = alpha,
colorFilter = colorFilter,
)
is CurrencyIconState.FiatIcon -> CoinIcon(
modifier = modifier,
url = icon.url,
fallbackResId = icon.fallbackResId,
alpha = alpha,
colorFilter = colorFilter,
)
is CurrencyIconState.TokenIcon -> TokenIcon(
modifier = modifier,
url = icon.url,

View file

@ -39,6 +39,7 @@ fun CurrencyIcon(state: CurrencyIconState, modifier: Modifier = Modifier, should
is CurrencyIconState.Locked -> LockedIcon(modifier = iconModifier)
is CurrencyIconState.Empty -> EmptyIcon(resId = state.resId, modifier = iconModifier)
is CurrencyIconState.CoinIcon,
is CurrencyIconState.FiatIcon,
is CurrencyIconState.CustomTokenIcon,
is CurrencyIconState.TokenIcon,
-> {

View file

@ -71,6 +71,21 @@ sealed class CurrencyIconState {
override val showCustomBadge: Boolean = true,
) : CurrencyIconState()
/**
* Represents a fiat icon.
*
* @property url The URL where the fiat icon can be fetched from. May be `null` if not found.
* @property fallbackResId The drawable resource ID to be used as a fallback if the URL is not available.
*/
data class FiatIcon(
val url: String?,
@DrawableRes val fallbackResId: Int,
) : CurrencyIconState() {
override val isGrayscale: Boolean = false
override val showCustomBadge: Boolean = false
override val topBadgeIconResId: Int? = null
}
data object Loading : CurrencyIconState() {
override val isGrayscale: Boolean = false
override val showCustomBadge: Boolean = false
@ -113,6 +128,7 @@ sealed class CurrencyIconState {
is Loading,
is Locked,
is Empty,
is FiatIcon,
-> this
}
}

View file

@ -37,10 +37,10 @@ import com.tangem.core.ui.res.TangemThemePreview
fun InputRowApprox(
leftIcon: CurrencyIconState,
leftTitle: TextReference,
leftSubtitle: TextReference,
leftSubtitle: TextReference?,
rightIcon: CurrencyIconState,
rightTitle: TextReference,
rightSubtitle: TextReference,
rightSubtitle: TextReference?,
modifier: Modifier = Modifier,
leftTitleEllipsisOffset: Int = 0,
rightTitleEllipsisOffset: Int = 0,
@ -69,7 +69,7 @@ fun InputRowApprox(
tint = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(
horizontal = TangemTheme.dimens.spacing4,
horizontal = TangemTheme.dimens.spacing8,
vertical = TangemTheme.dimens.spacing10,
),
)
@ -88,12 +88,13 @@ fun InputRowApprox(
private fun InputRowApproxItem(
iconState: CurrencyIconState,
title: TextReference,
subtitle: TextReference,
subtitle: TextReference?,
modifier: Modifier = Modifier,
titleEllipsisOffset: Int = 0,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
CurrencyIcon(
state = iconState,
@ -112,13 +113,15 @@ private fun InputRowApproxItem(
color = TangemTheme.colors.text.primary1,
ellipsis = TextEllipsis.OffsetEnd(titleEllipsisOffset),
)
EllipsisText(
text = subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing2),
)
if (subtitle != null) {
EllipsisText(
text = subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing2),
)
}
}
}
}
@ -154,6 +157,18 @@ private fun InputRowApproxPreview() {
modifier = Modifier
.background(TangemTheme.colors.background.action),
)
InputRowApprox(
leftIcon = CurrencyIconState.Loading,
leftTitle = TextReference.Str("Left title Left title Left title Left title Left title USD"),
leftSubtitle = null,
leftTitleEllipsisOffset = 3,
rightIcon = CurrencyIconState.Loading,
rightTitle = TextReference.Str("Right title USD"),
rightSubtitle = null,
rightTitleEllipsisOffset = 3,
modifier = Modifier
.background(TangemTheme.colors.background.action),
)
}
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.data.onramp
import com.tangem.data.onramp.converters.TransactionConverter
import com.tangem.data.onramp.models.OnrampTransactionDTO
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSet
@ -19,17 +21,21 @@ internal class DefaultOnrampTransactionRepository(
private val dispatchers: CoroutineDispatcherProvider,
) : OnrampTransactionRepository {
private val transactionConverter = TransactionConverter()
override suspend fun storeTransaction(transaction: OnrampTransaction) {
withContext(dispatchers.io) {
appPreferencesStore.editData { mutablePreferences ->
val stored = mutablePreferences.getObjectSet<OnrampTransaction>(
val stored = mutablePreferences.getObjectSet<OnrampTransactionDTO>(
PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
)
val updated = stored?.toMutableSet()
?.addOrReplace(transaction) { it.txId == transaction.txId }
?: mutableSetOf(transaction)
)?.map(transactionConverter::convert) ?: mutableSetOf()
mutablePreferences.setObjectSet(
val updated = stored.toMutableSet()
.addOrReplace(transaction) { it.txId == transaction.txId }
.map(transactionConverter::convertBack)
.toSet()
mutablePreferences.setObjectSet<OnrampTransactionDTO>(
key = PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
value = updated,
)
@ -38,9 +44,9 @@ internal class DefaultOnrampTransactionRepository(
}
override suspend fun getTransactionById(txId: String): OnrampTransaction? = withContext(dispatchers.io) {
val stored = appPreferencesStore.getObjectSetSync<OnrampTransaction>(
val stored = appPreferencesStore.getObjectSetSync<OnrampTransactionDTO>(
PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
)
).map(transactionConverter::convert)
stored.firstOrNull { it.txId == txId }
}
@ -49,24 +55,24 @@ internal class DefaultOnrampTransactionRepository(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): Flow<List<OnrampTransaction>> = appPreferencesStore
.getObjectSet<OnrampTransaction>(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY)
.getObjectSet<OnrampTransactionDTO>(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY)
.map { transactions ->
transactions.filter {
it.userWalletId == userWalletId && it.toCurrencyId == cryptoCurrencyId.value
}
}.map(transactionConverter::convert)
}
override suspend fun removeTransaction(txId: String) {
withContext(dispatchers.io) {
appPreferencesStore.editData { mutablePreferences ->
runCatching {
val stored = mutablePreferences.getObjectSet<OnrampTransaction>(
val stored = mutablePreferences.getObjectSet<OnrampTransactionDTO>(
PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
)?.toMutableSet()
stored?.removeIf { it.txId == txId }
mutablePreferences.setObjectSet(
mutablePreferences.setObjectSet<OnrampTransactionDTO>(
key = PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
value = stored ?: emptySet(),
)

View file

@ -0,0 +1,49 @@
package com.tangem.data.onramp.converters
import com.tangem.data.onramp.models.OnrampTransactionDTO
import com.tangem.datasource.api.onramp.models.response.Status
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.utils.converter.TwoWayConverter
internal class TransactionConverter : TwoWayConverter<OnrampTransactionDTO, OnrampTransaction> {
private val currencyConverter = CurrencyConverter()
override fun convert(value: OnrampTransactionDTO): OnrampTransaction {
return OnrampTransaction(
txId = value.txId,
userWalletId = value.userWalletId,
fromAmount = value.fromAmount,
fromCurrency = currencyConverter.convert(value.fromCurrency),
toAmount = value.toAmount,
toCurrencyId = value.toCurrencyId,
status = OnrampStatus.Status.valueOf(value.status.name),
externalTxUrl = value.externalTxUrl,
externalTxId = value.externalTxId,
timestamp = value.timestamp,
providerName = value.providerName,
providerImageUrl = value.providerImageUrl,
providerType = value.providerType,
)
}
override fun convertBack(value: OnrampTransaction): OnrampTransactionDTO {
return OnrampTransactionDTO(
txId = value.txId,
userWalletId = value.userWalletId,
fromAmount = value.fromAmount,
fromCurrency = currencyConverter.convertBack(value.fromCurrency),
toAmount = value.toAmount,
toCurrencyId = value.toCurrencyId,
status = Status.valueOf(value.status.name),
externalTxUrl = value.externalTxUrl,
externalTxId = value.externalTxId,
timestamp = value.timestamp,
providerName = value.providerName,
providerImageUrl = value.providerImageUrl,
providerType = value.providerType,
)
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.data.onramp.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.onramp.models.response.Status
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.domain.core.serialization.SerializedBigDecimal
import com.tangem.domain.wallets.models.UserWalletId
/**
* Model for local storing onramp transaction
*
* @property txId Inner express tx id
* @property userWalletId Wallet where tx was performed
* @property fromAmount Onramp amount to buy crypto
* @property fromCurrency Onramp currency
* @property toAmount Receiving crypto amount
* @property toCurrencyId Receiving crypto currency id [CryptoCurrency.ID.value]
* @property status Transaction status
* @property externalTxUrl Link to transaction on provider side
* @property externalTxId Id of transaction on provider side
* @property timestamp Transaction create time
* @property providerName Provider name
* @property providerImageUrl Provider image link
* @property providerType Provider type
*/
@JsonClass(generateAdapter = true)
data class OnrampTransactionDTO(
@Json(name = "txId")
val txId: String,
@Json(name = "userWalletId")
val userWalletId: UserWalletId,
@Json(name = "fromAmount")
val fromAmount: SerializedBigDecimal,
@Json(name = "fromCurrency")
val fromCurrency: OnrampCurrencyDTO,
@Json(name = "toAmount")
val toAmount: SerializedBigDecimal,
@Json(name = "toCurrencyId")
val toCurrencyId: String,
@Json(name = "status")
val status: Status,
@Json(name = "externalTxUrl")
val externalTxUrl: String?,
@Json(name = "externalTxId")
val externalTxId: String?,
@Json(name = "timestamp")
val timestamp: Long,
@Json(name = "providerName")
val providerName: String, // todo onramp fix after SwapProvider moved to own module
@Json(name = "providerImageUrl")
val providerImageUrl: String, // todo onramp fix after SwapProvider moved to own module
@Json(name = "providerType")
val providerType: String, // todo onramp fix after SwapProvider moved to own module
)

View file

@ -1,8 +1,5 @@
package com.tangem.domain.onramp.model
import kotlinx.serialization.Serializable
@Serializable
data class OnrampStatus(
val txId: String,
val providerId: String,

View file

@ -2,10 +2,26 @@ package com.tangem.domain.onramp.model.cache
import com.tangem.domain.core.serialization.SerializedBigDecimal
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable
@Serializable
/**
* Model for local storing onramp transaction
*
* @property txId Inner express tx id
* @property userWalletId Wallet where tx was performed
* @property fromAmount Onramp amount to buy crypto
* @property fromCurrency Onramp currency
* @property toAmount Receiving crypto amount
* @property toCurrencyId Receiving crypto currency id [CryptoCurrency.ID.value]
* @property status Transaction status
* @property externalTxUrl Link to transaction on provider side
* @property externalTxId Id of transaction on provider side
* @property timestamp Transaction create time
* @property providerName Provider name
* @property providerImageUrl Provider image link
* @property providerType Provider type
*/
data class OnrampTransaction(
val txId: String,
val userWalletId: UserWalletId,
@ -13,6 +29,11 @@ data class OnrampTransaction(
val fromCurrency: OnrampCurrency,
val toAmount: SerializedBigDecimal,
val toCurrencyId: String,
val providerName: String,
val providerImageUrl: String,
val status: OnrampStatus.Status,
val externalTxUrl: String?,
val externalTxId: String?,
val timestamp: Long,
val providerName: String, // todo onramp fix after SwapProvider moved to own module
val providerImageUrl: String, // todo onramp fix after SwapProvider moved to own module
val providerType: String, // todo onramp fix after SwapProvider moved to own module
)

View file

@ -80,6 +80,8 @@ dependencies {
implementation(projects.domain.transaction)
implementation(projects.domain.staking)
implementation(projects.domain.markets.models)
implementation(projects.domain.onramp)
implementation(projects.domain.onramp.models)
/** Temp dependency to swap domain */
implementation(projects.features.swap.domain)

View file

@ -308,6 +308,7 @@ internal object TokenDetailsPreviewData {
dialogConfig = null,
pendingTxs = persistentListOf(),
swapTxs = persistentListOf(),
onrampTxs = persistentListOf(),
pullToRefreshConfig = pullToRefreshConfig,
bottomSheetConfig = null,
isBalanceHidden = false,
@ -338,6 +339,7 @@ internal object TokenDetailsPreviewData {
dialogConfig = null,
pendingTxs = persistentListOf(),
swapTxs = persistentListOf(),
onrampTxs = persistentListOf(),
pullToRefreshConfig = pullToRefreshConfig,
bottomSheetConfig = null,
isBalanceHidden = false,

View file

@ -25,7 +25,7 @@ internal data class SwapTransactionsState(
val toFiatAmount: String,
val toCurrencyIcon: CurrencyIconState,
val fromCryptoCurrency: CryptoCurrency,
val fromCryptoAmount: String,
val fromCryptoAmount: TextReference,
val fromFiatAmount: String,
val fromCurrencyIcon: CurrencyIconState,
val showProviderLink: Boolean,

View file

@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
@ -21,6 +22,7 @@ internal data class TokenDetailsState(
val notifications: ImmutableList<TokenDetailsNotification>,
val pendingTxs: PersistentList<TransactionState>,
val swapTxs: PersistentList<SwapTransactionsState>,
val onrampTxs: PersistentList<ExpressTransactionStateUM.OnrampUM>,
val txHistoryState: TxHistoryState,
val dialogConfig: TokenDetailsDialogConfig?,
val pullToRefreshConfig: PullToRefreshConfig,

View file

@ -0,0 +1,57 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.express
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications
import kotlinx.collections.immutable.ImmutableList
internal sealed class ExpressTransactionStateUM {
abstract val info: ExpressTransactionStateInfoUM
data class ExchangeUM(
override val info: ExpressTransactionStateInfoUM,
val provider: SwapProvider,
val activeStatus: ExchangeStatus?,
val statuses: ImmutableList<ExchangeStatusState>,
val notification: ExchangeStatusNotifications? = null,
val showProviderLink: Boolean,
val isRefundTerminalStatus: Boolean,
) : ExpressTransactionStateUM()
data class OnrampUM(
override val info: ExpressTransactionStateInfoUM,
val providerName: String, // todo onramp fix after SwapProvider moved to own module
val providerImageUrl: String, // todo onramp fix after SwapProvider moved to own module
val providerType: String, // todo onramp fix after SwapProvider moved to own module
val cryptoCurrencyName: String,
val activeStatus: OnrampStatus.Status,
) : ExpressTransactionStateUM()
}
internal data class ExpressTransactionStateInfoUM(
val status: ExpressStatusUM,
val notification: NotificationUM?,
val txId: String,
val txUrl: String?,
val txExternalId: String?,
val timestamp: TextReference,
val onGoToProviderClick: (String) -> Unit,
val onClick: () -> Unit,
val toAmount: TextReference,
val toFiatAmount: TextReference?,
val toAmountSymbol: String,
val toCurrencyIcon: CurrencyIconState,
val fromAmount: TextReference,
val fromFiatAmount: TextReference?,
val fromAmountSymbol: String,
val fromCurrencyIcon: CurrencyIconState,
)

View file

@ -0,0 +1,245 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.common.ui.notifications.ExpressNotificationsUM
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateInfoUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
internal class TokenDetailsOnrampTransactionStateConverter(
private val clickIntents: TokenDetailsClickIntents,
private val cryptoCurrency: CryptoCurrency,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val appCurrencyProvider: Provider<AppCurrency>,
) : Converter<OnrampTransaction, ExpressTransactionStateUM.OnrampUM> {
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
override fun convert(value: OnrampTransaction): ExpressTransactionStateUM.OnrampUM {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val appCurrency = appCurrencyProvider()
return ExpressTransactionStateUM.OnrampUM(
info = ExpressTransactionStateInfoUM(
status = convertStatuses(value.status, value.externalTxUrl),
notification = getNotification(value.status, value.externalTxUrl),
txId = value.txId,
txExternalId = value.externalTxId,
txUrl = value.externalTxUrl,
timestamp = resourceReference(
R.string.send_date_format,
wrappedList(
value.timestamp.toTimeFormat(DateTimeFormatters.dateFormatter),
value.timestamp.toTimeFormat(),
),
),
toAmount = stringReference(
value.toAmount.format { crypto(cryptoCurrency) },
),
toFiatAmount = stringReference(
cryptoCurrencyStatus?.value?.fiatRate?.multiply(value.toAmount).format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
),
toAmountSymbol = cryptoCurrency.symbol,
toCurrencyIcon = iconStateConverter.convert(cryptoCurrency),
fromAmount = stringReference(
value.fromAmount.format {
fiat(
fiatCurrencyCode = value.fromCurrency.name,
fiatCurrencySymbol = value.fromCurrency.code,
)
},
),
fromFiatAmount = null,
fromAmountSymbol = value.fromCurrency.code,
fromCurrencyIcon = CurrencyIconState.FiatIcon(
url = value.fromCurrency.image,
fallbackResId = R.drawable.ic_currency_24,
),
onGoToProviderClick = clickIntents::onGoToProviderClick,
onClick = { clickIntents.onOnrampTransactionClick(value.txId) },
),
providerName = value.providerName,
providerImageUrl = value.providerImageUrl,
providerType = value.providerType,
cryptoCurrencyName = cryptoCurrency.name,
activeStatus = value.status,
)
}
private fun getNotification(status: OnrampStatus.Status, externalTxUrl: String?): NotificationUM? {
if (externalTxUrl == null) return null
return when (status) {
OnrampStatus.Status.Verifying -> {
ExpressNotificationsUM.NeedVerification {
clickIntents.onGoToProviderClick(externalTxUrl)
}
}
OnrampStatus.Status.Failed -> {
ExpressNotificationsUM.FailedByProvider {
clickIntents.onGoToProviderClick(externalTxUrl)
}
}
else -> null
}
}
private fun convertStatuses(status: OnrampStatus.Status, externalTxUrl: String?): ExpressStatusUM {
val statuses = with(status) {
persistentListOf(
getAwaitingDepositItem(),
getPaymentProcessingItem(),
getBuyingItem(),
getSendingItem(),
)
}
return ExpressStatusUM(
title = resourceReference(R.string.common_transaction_status),
link = getStatusLink(status, externalTxUrl),
statuses = statuses,
)
}
private fun OnrampStatus.Status.getAwaitingDepositItem() = ExpressStatusItemUM(
text = when {
order < OnrampStatus.Status.WaitingForPayment.order -> {
resourceReference(R.string.express_exchange_status_receiving)
}
this == OnrampStatus.Status.WaitingForPayment -> {
resourceReference(R.string.express_exchange_status_receiving_active)
}
else -> {
resourceReference(R.string.express_exchange_status_received)
}
},
state = getStatusState(OnrampStatus.Status.WaitingForPayment),
)
private fun OnrampStatus.Status.getPaymentProcessingItem() = ExpressStatusItemUM(
text = when {
order < OnrampStatus.Status.PaymentProcessing.order -> {
resourceReference(R.string.express_exchange_status_confirming)
}
this == OnrampStatus.Status.PaymentProcessing -> {
resourceReference(R.string.express_exchange_status_confirming_active)
}
this == OnrampStatus.Status.Verifying -> {
resourceReference(R.string.express_exchange_status_verifying)
}
this == OnrampStatus.Status.Failed -> {
resourceReference(R.string.express_exchange_status_failed)
}
else -> resourceReference(R.string.express_exchange_status_confirmed)
},
state = when {
order < OnrampStatus.Status.PaymentProcessing.order -> {
ExpressStatusItemState.Default
}
this == OnrampStatus.Status.PaymentProcessing -> {
ExpressStatusItemState.Active
}
this == OnrampStatus.Status.Verifying -> {
ExpressStatusItemState.Warning
}
this == OnrampStatus.Status.Failed -> {
ExpressStatusItemState.Error
}
else -> {
ExpressStatusItemState.Done
}
},
)
private fun OnrampStatus.Status.getBuyingItem() = ExpressStatusItemUM(
text = when {
order < OnrampStatus.Status.Paid.order -> {
resourceReference(R.string.express_status_buying, wrappedList(cryptoCurrency.name))
}
this == OnrampStatus.Status.Paid -> {
resourceReference(
R.string.express_status_buying_active,
wrappedList(cryptoCurrency.name),
)
}
else -> {
resourceReference(R.string.express_status_bought, wrappedList(cryptoCurrency.name))
}
},
state = getStatusState(OnrampStatus.Status.Paid),
)
private fun OnrampStatus.Status.getSendingItem() = ExpressStatusItemUM(
text = when {
order < OnrampStatus.Status.Sending.order -> {
resourceReference(
R.string.express_exchange_status_sending,
wrappedList(cryptoCurrency.name),
)
}
this == OnrampStatus.Status.Sending -> {
resourceReference(
R.string.express_exchange_status_sending_active,
wrappedList(cryptoCurrency.name),
)
}
else -> {
resourceReference(
R.string.express_exchange_status_sent,
wrappedList(cryptoCurrency.name),
)
}
},
state = getStatusState(OnrampStatus.Status.Sending),
)
private fun getStatusLink(status: OnrampStatus.Status, externalTxUrl: String?): ExpressLinkUM {
if (externalTxUrl == null) return ExpressLinkUM.Empty
return when (status) {
OnrampStatus.Status.Verifying,
OnrampStatus.Status.Failed,
-> {
ExpressLinkUM.Content(
icon = R.drawable.ic_arrow_top_right_24,
text = resourceReference(R.string.common_go_to_provider),
onClick = {
clickIntents.onGoToProviderClick(externalTxUrl)
},
)
}
else -> ExpressLinkUM.Empty
}
}
private fun OnrampStatus.Status.getStatusState(targetState: OnrampStatus.Status) = when {
order < targetState.order -> ExpressStatusItemState.Default
this == targetState -> ExpressStatusItemState.Active
else -> ExpressStatusItemState.Done
}
}

View file

@ -67,6 +67,7 @@ internal class TokenDetailsSkeletonStateConverter(
notifications = persistentListOf(),
pendingTxs = persistentListOf(),
swapTxs = persistentListOf(),
onrampTxs = persistentListOf(),
txHistoryState = TxHistoryState.Content(
contentItems = MutableStateFlow(
value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick),

View file

@ -36,7 +36,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter.TokenDetailsLoadingTxHistoryModel
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider

View file

@ -4,6 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
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.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.BigDecimalFormatter
@ -94,7 +95,9 @@ internal class TokenDetailsSwapTransactionsStateConverter(
toFiatAmount = getFiatAmount(toFiatAmount),
toCurrencyIcon = iconStateConverter.convert(toCryptoCurrency),
fromCryptoCurrency = fromCryptoCurrency,
fromCryptoAmount = fromAmount.format { crypto(fromCryptoCurrency) },
fromCryptoAmount = stringReference(
fromAmount.format { crypto(fromCryptoCurrency) },
),
fromFiatAmount = getFiatAmount(fromFiatAmount),
fromCurrencyIcon = iconStateConverter.convert(fromCryptoCurrency),
showProviderLink = showProviderLink,

View file

@ -1,4 +1,4 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.swaptx.ExchangeAnalyticsStatus
@ -17,7 +17,8 @@ import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.PersistentList

View file

@ -0,0 +1,128 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.onramp.GetOnrampStatusUseCase
import com.tangem.domain.onramp.GetOnrampTransactionsUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.onramp.OnrampSaveTransactionUseCase
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsOnrampTransactionStateConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp.OnrampStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.withContext
@Suppress("LongParameterList")
internal class OnrampStatusFactory(
private val stateProvider: Provider<TokenDetailsState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val clickIntents: TokenDetailsClickIntents,
private val cryptoCurrency: CryptoCurrency,
private val userWalletId: UserWalletId,
private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase,
private val onrampSaveTransactionUseCase: OnrampSaveTransactionUseCase,
private val getOnrampStatusUseCase: GetOnrampStatusUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
private val dispatchers: CoroutineDispatcherProvider,
) {
private val onrampTransactionStateConverter by lazy(LazyThreadSafetyMode.NONE) {
TokenDetailsOnrampTransactionStateConverter(
clickIntents = clickIntents,
cryptoCurrency = cryptoCurrency,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
appCurrencyProvider = appCurrencyProvider,
)
}
operator fun invoke(): Flow<List<OnrampTransaction>> {
return getOnrampTransactionsUseCase(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrency.id,
).fold(
ifRight = { savedTransactions ->
savedTransactions
},
ifLeft = { flowOf(persistentListOf()) },
)
}
fun getStateWithOnrampStatusBottomSheet(onrampTxState: ExpressTransactionStateUM.OnrampUM): TokenDetailsState {
return stateProvider().copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissBottomSheet,
content = OnrampStatusBottomSheetConfig(onrampTxState),
),
)
}
fun updateOnrampStatusBottomSheet(onrampTxs: List<OnrampTransaction>): TokenDetailsState {
val state = stateProvider()
val bottomSheetConfig = state.bottomSheetConfig
val onrampBottomSheet = bottomSheetConfig?.content as? OnrampStatusBottomSheetConfig
val updatedOnrampTxState = onrampTransactionStateConverter.convertList(onrampTxs).toPersistentList()
val currentOnrampTxState =
updatedOnrampTxState.firstOrNull { it.info.txId == onrampBottomSheet?.value?.info?.txId }
return state.copy(
onrampTxs = updatedOnrampTxState,
bottomSheetConfig = bottomSheetConfig?.copy(
content = if (currentOnrampTxState != null && currentOnrampTxState != onrampBottomSheet?.value) {
OnrampStatusBottomSheetConfig(currentOnrampTxState)
} else {
onrampBottomSheet
} ?: bottomSheetConfig.content,
),
)
}
suspend fun removeTransactionOnBottomSheetClosed(): TokenDetailsState {
val state = stateProvider()
val bottomSheetConfig = state.bottomSheetConfig?.content as? OnrampStatusBottomSheetConfig ?: return state
val selectedTx = bottomSheetConfig.value
return if (selectedTx.activeStatus.isTerminal()) {
onrampRemoveTransactionUseCase(txId = selectedTx.info.txId)
val filteredTxs = state.onrampTxs
.filterNot { it.info.txId == selectedTx.info.txId }
.toPersistentList()
state.copy(onrampTxs = filteredTxs)
} else {
state
}
}
suspend fun updateOnrmapTxStatuses(onrampTxList: List<OnrampTransaction>) = withContext(dispatchers.io) {
onrampTxList.map { tx ->
async {
getOnrampStatusUseCase(tx.txId).fold(
ifLeft = { null },
ifRight = { statusModel ->
val updatedStatus = tx.copy(status = statusModel.status)
onrampSaveTransactionUseCase(updatedStatus)
updatedStatus
},
)
}
}
.awaitAll()
.filterNotNull()
.toPersistentList()
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@ -49,15 +48,18 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.swapTransactionsItems
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange.ExchangeStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange.swapTransactionsItems
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp.OnrampStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp.OnrampStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp.onrampTransactionsItems
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
// TODO: Split to blocks [REDACTED_JIRA]
@Suppress("LongMethod")
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockComponent: TokenMarketBlockComponent?) {
BackHandler(onBack = state.topAppBarConfig.onBackClick)
@ -127,11 +129,11 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon
if (it is TokenDetailsNotification.SwapPromo) {
OkxPromoNotification(
config = it.config,
modifier = itemModifier.animateItemPlacement(),
modifier = itemModifier.animateItem(),
)
} else {
Notification(
modifier = itemModifier.animateItemPlacement(),
modifier = itemModifier.animateItem(),
config = it.config,
iconTint = when (it) {
is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent
@ -179,8 +181,13 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon
}
swapTransactionsItems(
state.swapTxs,
itemModifier,
swapTxs = state.swapTxs,
modifier = itemModifier,
)
onrampTransactionsItems(
onrampTxs = state.onrampTxs,
modifier = itemModifier,
)
txHistoryItems(
@ -210,6 +217,9 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon
is ExchangeStatusBottomSheetConfig -> {
ExchangeStatusBottomSheet(config = config)
}
is OnrampStatusBottomSheetConfig -> {
OnrampStatusBottomSheet(config = config)
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -17,7 +17,7 @@ import com.tangem.features.tokendetails.impl.R
@Suppress("LongParameterList")
@Composable
internal fun ExchangeEstimate(
internal fun ExpressEstimate(
timestamp: TextReference,
fromTokenIconState: CurrencyIconState,
toTokenIconState: CurrencyIconState,
@ -25,8 +25,8 @@ internal fun ExchangeEstimate(
fromCryptoSymbol: String,
toCryptoAmount: TextReference,
toCryptoSymbol: String,
fromFiatAmount: TextReference,
toFiatAmount: TextReference,
fromFiatAmount: TextReference?,
toFiatAmount: TextReference?,
modifier: Modifier = Modifier,
) {
Column(

View file

@ -1,5 +1,6 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
import android.content.res.Configuration
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -17,15 +18,18 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
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.SpacerWMax
import com.tangem.core.ui.components.inputrow.InputRowBestRate
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
internal fun ExchangeProvider(
internal fun ExpressProvider(
providerName: TextReference,
providerType: TextReference,
providerTxId: String?,
@ -39,7 +43,7 @@ internal fun ExchangeProvider(
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
) {
Box(
Row(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing12)
@ -49,12 +53,12 @@ internal fun ExchangeProvider(
text = stringResource(id = R.string.express_provider),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.align(Alignment.CenterStart),
)
SpacerWMax()
if (!providerTxId.isNullOrEmpty()) {
Row(
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(start = 8.dp)
.clickable {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clipboardManager.setText(AnnotatedString(providerTxId))
@ -73,6 +77,8 @@ internal fun ExchangeProvider(
Text(
modifier = Modifier.align(Alignment.CenterVertically),
text = stringResource(R.string.express_transaction_id, providerTxId),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
@ -88,14 +94,15 @@ internal fun ExchangeProvider(
}
}
@Preview(showBackground = true)
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ExchangeProvider_Preview() {
TangemThemePreview(isDark = false) {
ExchangeProvider(
private fun ExpressProvider_Preview() {
TangemThemePreview {
ExpressProvider(
providerName = TextReference.Str("Changelly"),
providerType = TextReference.Str("CEX"),
providerTxId = "hjsbajcqb",
providerTxId = "hjsbajcqbhjsbajcqbhjsbajcqbhjsbajcqbhjsbajcqb",
imageUrl = "",
)
}

View file

@ -1,14 +1,12 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -16,7 +14,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@ -25,60 +22,25 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
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.TangemThemePreview
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.PersistentList
@OptIn(ExperimentalFoundationApi::class)
internal fun LazyListScope.swapTransactionsItems(
swapTxs: PersistentList<SwapTransactionsState>,
modifier: Modifier = Modifier,
) {
if (swapTxs.isNotEmpty()) {
items(
count = swapTxs.size,
key = { swapTxs[it].txId },
contentType = { swapTxs[it]::class.java },
) {
val item = swapTxs[it]
val (iconRes, tint) = when (item.activeStatus) {
ExchangeStatus.Verifying -> R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention
ExchangeStatus.Failed, ExchangeStatus.Cancelled -> {
R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning
}
else -> null to null
}
ExchangeStatusItem(
providerName = item.provider.name,
fromTokenIconState = item.fromCurrencyIcon,
toTokenIconState = item.toCurrencyIcon,
fromAmount = item.fromCryptoAmount,
fromSymbol = item.fromCryptoCurrency.symbol,
toSymbol = item.toCryptoCurrency.symbol,
onClick = item.onClick,
infoIconRes = iconRes,
infoIconTint = tint,
modifier = modifier.animateItemPlacement(),
)
}
}
}
@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod", "LongParameterList")
@Composable
private fun ExchangeStatusItem(
providerName: String,
internal fun ExpressStatusItem(
title: TextReference,
fromTokenIconState: CurrencyIconState,
toTokenIconState: CurrencyIconState,
fromAmount: String,
fromAmount: TextReference,
fromSymbol: String,
toSymbol: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
toAmount: TextReference = TextReference.EMPTY,
@DrawableRes infoIconRes: Int? = null,
infoIconTint: Color? = null,
) {
@ -94,7 +56,7 @@ private fun ExchangeStatusItem(
val padding6 = TangemTheme.dimens.spacing6
Text(
text = stringResource(id = R.string.express_exchange_by, providerName),
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.constrainAs(titleRef) {
@ -114,7 +76,7 @@ private fun ExchangeStatusItem(
},
)
EllipsisText(
text = fromAmount,
text = fromAmount.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
ellipsis = TextEllipsis.OffsetEnd(fromSymbol.length),
@ -151,10 +113,11 @@ private fun ExchangeStatusItem(
bottom.linkTo(parent.bottom)
},
)
Text(
text = toSymbol,
EllipsisText(
text = toAmount.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
ellipsis = TextEllipsis.OffsetEnd(toSymbol.length),
modifier = Modifier.constrainAs(toRef) {
start.linkTo(toIconRef.end, padding6)
top.linkTo(titleRef.bottom, padding6)
@ -199,16 +162,17 @@ private fun ExchangeStatusItem(
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ExchangeStatusItemPreview(
@PreviewParameter(ExchangeStatusItemsPreviewParameterProvider::class) amount: String,
private fun ExpressStatusItemPreview(
@PreviewParameter(ExpressStatusItemPreviewParameterProvider::class) amount: String,
) {
TangemThemePreview {
ExchangeStatusItem(
providerName = "ChangeNow",
ExpressStatusItem(
title = stringReference("ChangeNow"),
fromTokenIconState = CurrencyIconState.Loading,
toTokenIconState = CurrencyIconState.Loading,
fromAmount = amount,
fromAmount = stringReference(amount),
fromSymbol = "USDT",
toAmount = stringReference(amount),
toSymbol = "USDT",
onClick = {},
infoIconRes = null,
@ -217,7 +181,7 @@ private fun ExchangeStatusItemPreview(
}
}
private class ExchangeStatusItemsPreviewParameterProvider : PreviewParameterProvider<String> {
private class ExpressStatusItemPreviewParameterProvider : PreviewParameterProvider<String> {
override val values: Sequence<String>
get() = sequenceOf(
"1111111111111111111111111111 USDT",

View file

@ -1,4 +1,4 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent

View file

@ -1,4 +1,4 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Column
@ -23,6 +23,8 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressEstimate
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressProvider
@Composable
internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) {
@ -54,11 +56,11 @@ private fun ExchangeStatusBottomSheetContent(config: SwapTransactionsState) {
.align(CenterHorizontally),
)
SpacerH16()
ExchangeEstimate(
ExpressEstimate(
timestamp = config.timestamp,
fromTokenIconState = config.fromCurrencyIcon,
toTokenIconState = config.toCurrencyIcon,
fromCryptoAmount = TextReference.Str(config.fromCryptoAmount),
fromCryptoAmount = config.fromCryptoAmount,
fromCryptoSymbol = config.fromCryptoCurrency.symbol,
toCryptoAmount = TextReference.Str(config.toCryptoAmount),
toCryptoSymbol = config.toCryptoCurrency.symbol,
@ -66,7 +68,7 @@ private fun ExchangeStatusBottomSheetContent(config: SwapTransactionsState) {
toFiatAmount = TextReference.Str(config.toFiatAmount),
)
SpacerH12()
ExchangeProvider(
ExpressProvider(
providerName = TextReference.Str(config.provider.name),
providerType = TextReference.Str(config.provider.type.providerName),
providerTxId = config.txExternalId,

View file

@ -0,0 +1,47 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusItem
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.PersistentList
internal fun LazyListScope.swapTransactionsItems(
swapTxs: PersistentList<SwapTransactionsState>,
modifier: Modifier = Modifier,
) {
if (swapTxs.isNotEmpty()) {
items(
count = swapTxs.size,
key = { swapTxs[it].txId },
contentType = { swapTxs[it]::class.java },
) {
val item = swapTxs[it]
val (iconRes, tint) = when (item.activeStatus) {
ExchangeStatus.Verifying -> R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention
ExchangeStatus.Failed, ExchangeStatus.Cancelled -> {
R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning
}
else -> null to null
}
ExpressStatusItem(
title = resourceReference(id = R.string.express_exchange_by, wrappedList(item.provider.name)),
fromTokenIconState = item.fromCurrencyIcon,
toTokenIconState = item.toCurrencyIcon,
fromAmount = item.fromCryptoAmount,
fromSymbol = item.fromCryptoCurrency.symbol,
toSymbol = item.toCryptoCurrency.symbol,
onClick = item.onClick,
infoIconRes = iconRes,
infoIconTint = tint,
modifier = modifier.animateItem(),
)
}
}
}

View file

@ -0,0 +1,87 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import com.tangem.common.ui.expressStatus.ExpressStatusBlock
import com.tangem.common.ui.expressStatus.ExpressStatusNotificationBlock
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressEstimate
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressProvider
@Composable
internal fun OnrampStatusBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
containerColor = TangemTheme.colors.background.tertiary,
) { content: OnrampStatusBottomSheetConfig ->
OnrampStatusBottomSheetContent(config = content.value)
}
}
@Composable
private fun OnrampStatusBottomSheetContent(config: ExpressTransactionStateUM.OnrampUM) {
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) {
SpacerH10()
Text(
text = stringResource(id = R.string.common_transaction_status),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.align(CenterHorizontally),
)
SpacerH10()
Text(
text = stringResource(id = R.string.express_exchange_status_subtitle),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier
.align(CenterHorizontally),
)
SpacerH16()
ExpressEstimate(
timestamp = config.info.timestamp,
fromTokenIconState = config.info.fromCurrencyIcon,
toTokenIconState = config.info.toCurrencyIcon,
fromCryptoAmount = config.info.fromAmount,
fromCryptoSymbol = config.info.fromAmountSymbol,
toCryptoAmount = config.info.toAmount,
toCryptoSymbol = config.info.toAmountSymbol,
fromFiatAmount = config.info.fromFiatAmount,
toFiatAmount = config.info.toFiatAmount,
)
SpacerH12()
ExpressProvider(
providerName = stringReference(config.providerName),
providerType = stringReference(config.providerType),
providerTxId = config.info.txExternalId,
imageUrl = config.providerImageUrl,
)
SpacerH12()
ExpressStatusBlock(state = config.info.status)
if (config.info.notification != null) {
ExpressStatusNotificationBlock(state = config.info.notification)
}
SpacerH24()
}
}
internal data class OnrampStatusBottomSheetConfig(
val value: ExpressTransactionStateUM.OnrampUM,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,48 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusItem
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.PersistentList
internal fun LazyListScope.onrampTransactionsItems(
onrampTxs: PersistentList<ExpressTransactionStateUM.OnrampUM>,
modifier: Modifier = Modifier,
) {
items(
count = onrampTxs.size,
key = { onrampTxs[it].info.txId },
contentType = { onrampTxs[it]::class.java },
) {
val item = onrampTxs[it]
val itemInfo = item.info
val (iconRes, tint) = when (item.activeStatus) {
OnrampStatus.Status.Verifying -> R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention
OnrampStatus.Status.Failed -> {
R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning
}
else -> null to null
}
ExpressStatusItem(
title = resourceReference(id = R.string.express_status_buying, wrappedList(item.cryptoCurrencyName)),
fromTokenIconState = itemInfo.fromCurrencyIcon,
toTokenIconState = itemInfo.toCurrencyIcon,
fromAmount = itemInfo.fromAmount,
fromSymbol = itemInfo.fromAmountSymbol,
toAmount = itemInfo.toAmount,
toSymbol = itemInfo.toAmountSymbol,
onClick = item.info.onClick,
infoIconRes = iconRes,
infoIconTint = tint,
modifier = modifier.animateItem(),
)
}
}

View file

@ -47,6 +47,8 @@ interface TokenDetailsClickIntents {
fun onSwapTransactionClick(txId: String)
fun onOnrampTransactionClick(txId: String)
fun onGoToProviderClick(url: String)
fun onSwapPromoDismiss()

View file

@ -30,6 +30,11 @@ import com.tangem.domain.card.GetExtendedPublicKeyForCurrencyUseCase
import com.tangem.domain.card.NetworkHasDerivationUseCase
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.onramp.GetOnrampStatusUseCase
import com.tangem.domain.onramp.GetOnrampTransactionsUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.onramp.OnrampSaveTransactionUseCase
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
@ -71,7 +76,10 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTrans
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExchangeStatusFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.OnrampStatusFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp.OnrampStatusBottomSheetConfig
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
@ -122,6 +130,10 @@ internal class TokenDetailsViewModel @Inject constructor(
private val analyticsEventsHandler: AnalyticsEventHandler,
private val vibratorHapticManager: VibratorHapticManager,
private val clipboardManager: ClipboardManager,
private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase,
private val onrampSaveTransactionUseCase: OnrampSaveTransactionUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
private val getOnrampStatusUseCase: GetOnrampStatusUseCase,
getUserWalletUseCase: GetUserWalletUseCase,
getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase,
deepLinksRegistry: DeepLinksRegistry,
@ -146,12 +158,14 @@ internal class TokenDetailsViewModel @Inject constructor(
private val refreshStateJobHolder = JobHolder()
private val warningsJobHolder = JobHolder()
private val swapTxJobHolder = JobHolder()
private val onrampTxJobHolder = JobHolder()
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
private var stakingEntryInfo: StakingEntryInfo? = null
private var stakingAvailability: StakingAvailability = StakingAvailability.Unavailable
private var swapTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<SwapTransactionsState>>()
private var onrampTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<OnrampTransaction>>()
private val stateFactory = TokenDetailsStateFactory(
currentStateProvider = Provider { uiState.value },
@ -186,6 +200,22 @@ internal class TokenDetailsViewModel @Inject constructor(
)
}
private val onrampStatusFactory by lazy(LazyThreadSafetyMode.NONE) {
OnrampStatusFactory(
stateProvider = Provider { uiState.value },
appCurrencyProvider = Provider { selectedAppCurrencyFlow.value },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
clickIntents = this,
getOnrampTransactionsUseCase = getOnrampTransactionsUseCase,
onrampSaveTransactionUseCase = onrampSaveTransactionUseCase,
onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase,
getOnrampStatusUseCase = getOnrampStatusUseCase,
dispatchers = dispatchers,
)
}
private val notificationsAnalyticsSender by lazy(mode = LazyThreadSafetyMode.NONE) {
TokenDetailsNotificationsAnalyticsSender(
cryptoCurrency = cryptoCurrency,
@ -226,12 +256,15 @@ internal class TokenDetailsViewModel @Inject constructor(
override fun onCleared() {
swapTxStatusTaskScheduler.cancelTask()
onrampTxStatusTaskScheduler.cancelTask()
onrampTxJobHolder.cancel()
super.onCleared()
}
private fun updateContent() {
subscribeOnCurrencyStatusUpdates()
subscribeOnExchangeTransactionsUpdates()
subscribeOnOnrampTransactionsUpdates()
updateTxHistory(refresh = false, showItemsLoading = true)
updateStakingInfo()
@ -332,6 +365,38 @@ internal class TokenDetailsViewModel @Inject constructor(
}
}
private fun subscribeOnOnrampTransactionsUpdates() {
viewModelScope.launch(dispatchers.main) {
onrampTxStatusTaskScheduler.cancelTask()
onrampStatusFactory.invoke().distinctUntilChanged()
.filterNot { it.isEmpty() }
.onEach { onrampTxs ->
internalUiState.value = onrampStatusFactory.updateOnrampStatusBottomSheet(onrampTxs)
onrampTxStatusTaskScheduler.scheduleTask(
viewModelScope,
PeriodicTask(
delay = EXCHANGE_STATUS_UPDATE_DELAY,
task = {
runCatching {
val onrampTxsToUpdate = onrampTxs.filter { onrampTx ->
internalUiState.value.onrampTxs.any { it.info.txId == onrampTx.txId }
}
onrampStatusFactory.updateOnrmapTxStatuses(onrampTxsToUpdate)
}
},
onSuccess = {
internalUiState.value = onrampStatusFactory.updateOnrampStatusBottomSheet(onrampTxs)
},
onError = { /* no-op */ },
),
)
}
.flowOn(dispatchers.main)
.launchIn(viewModelScope)
.saveIn(onrampTxJobHolder)
}
}
private fun updateSwapTx(swapTxs: PersistentList<SwapTransactionsState>) {
val config = internalUiState.value.bottomSheetConfig
val exchangeBottomSheet = config?.content as? ExchangeStatusBottomSheetConfig
@ -771,6 +836,7 @@ internal class TokenDetailsViewModel @Inject constructor(
showItemsLoading = internalUiState.value.txHistoryState !is TxHistoryState.Content,
)
subscribeOnExchangeTransactionsUpdates()
subscribeOnOnrampTransactionsUpdates()
},
).awaitAll()
internalUiState.value = stateFactory.getRefreshedState()
@ -779,9 +845,16 @@ internal class TokenDetailsViewModel @Inject constructor(
override fun onDismissBottomSheet() {
val bsContent = internalUiState.value.bottomSheetConfig?.content
if (bsContent is ExchangeStatusBottomSheetConfig) {
viewModelScope.launch(dispatchers.main) {
internalUiState.value = exchangeStatusFactory.removeTransactionOnBottomSheetClosed()
when (bsContent) {
is ExchangeStatusBottomSheetConfig -> {
viewModelScope.launch(dispatchers.main) {
internalUiState.value = exchangeStatusFactory.removeTransactionOnBottomSheetClosed()
}
}
is OnrampStatusBottomSheetConfig -> {
viewModelScope.launch(dispatchers.main) {
internalUiState.value = onrampStatusFactory.removeTransactionOnBottomSheetClosed()
}
}
}
internalUiState.value = stateFactory.getStateWithClosedBottomSheet()
@ -797,6 +870,11 @@ internal class TokenDetailsViewModel @Inject constructor(
internalUiState.value = stateFactory.getStateWithExchangeStatusBottomSheet(swapTxState)
}
override fun onOnrampTransactionClick(txId: String) {
val onrampTxState = internalUiState.value.onrampTxs.first { it.info.txId == txId }
internalUiState.value = onrampStatusFactory.getStateWithOnrampStatusBottomSheet(onrampTxState)
}
override fun onGoToProviderClick(url: String) {
router.openUrl(url)
}