diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt index 8d1787952d..52d7c920a6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt @@ -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, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index 1b92960c51..a9a7677136 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -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, -> { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt index 371ad59f61..5ebf4ad190 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt @@ -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 } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt index 4a2a746428..2cc7bbdd3b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt @@ -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), + ) } } } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt index eaaad04a78..c31d07714c 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt @@ -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( + val stored = mutablePreferences.getObjectSet( 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( 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( + val stored = appPreferencesStore.getObjectSetSync( 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> = appPreferencesStore - .getObjectSet(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY) + .getObjectSet(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( + val stored = mutablePreferences.getObjectSet( PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY, )?.toMutableSet() stored?.removeIf { it.txId == txId } - mutablePreferences.setObjectSet( + mutablePreferences.setObjectSet( key = PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY, value = stored ?: emptySet(), ) diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/TransactionConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/TransactionConverter.kt new file mode 100644 index 0000000000..5373cba864 --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/TransactionConverter.kt @@ -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 { + + 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, + ) + } +} \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTransactionDTO.kt b/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTransactionDTO.kt new file mode 100644 index 0000000000..a5b8b56612 --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTransactionDTO.kt @@ -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 +) \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt index 69ecefb695..6547e2e546 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt @@ -1,8 +1,5 @@ package com.tangem.domain.onramp.model -import kotlinx.serialization.Serializable - -@Serializable data class OnrampStatus( val txId: String, val providerId: String, diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt index 96f069b02f..d2403d003e 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt @@ -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 ) \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 32efd6bb49..ac41a99f20 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -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) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 3f49fe9fb2..3df712af86 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -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, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt index e79f4acb6a..88b4fbfae5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -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, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index a3fbf4d4fb..4e2755339a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -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, val pendingTxs: PersistentList, val swapTxs: PersistentList, + val onrampTxs: PersistentList, val txHistoryState: TxHistoryState, val dialogConfig: TokenDetailsDialogConfig?, val pullToRefreshConfig: PullToRefreshConfig, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/express/ExpressTransactionStateUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/express/ExpressTransactionStateUM.kt new file mode 100644 index 0000000000..c4f11dd934 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/express/ExpressTransactionStateUM.kt @@ -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, + 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, +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt new file mode 100644 index 0000000000..e50c7e3977 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt @@ -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, + private val appCurrencyProvider: Provider, +) : Converter { + + 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 + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 98850f08f2..89df9ace33 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -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), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 23aa2143f1..4b31124dc3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -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 diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index fc0cfa0541..5f36f79e34 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -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, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt similarity index 98% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt rename to features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt index aa63ffd144..262493e39a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt @@ -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 diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt new file mode 100644 index 0000000000..9d9f0cee0e --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -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, + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + 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> { + 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): 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) = 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() + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 1c953617df..02a7c85aad 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -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) + } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressEstimate.kt similarity index 95% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt rename to features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressEstimate.kt index 319e6f44fe..575ea130a4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressEstimate.kt @@ -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( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressProvider.kt similarity index 85% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt rename to features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressProvider.kt index 7d1c2f14b5..860204ee74 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressProvider.kt @@ -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 = "", ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusItem.kt similarity index 73% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt rename to features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusItem.kt index 4b2a5a1ab4..698c5aa938 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusItem.kt @@ -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, - 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 { +private class ExpressStatusItemPreviewParameterProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( "1111111111111111111111111111 USDT", diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBlock.kt similarity index 99% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt rename to features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBlock.kt index 7831a8aba3..6e8205895c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBlock.kt @@ -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 diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheet.kt similarity index 93% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt rename to features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheet.kt index d3372936b1..feeb5ca411 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheet.kt @@ -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, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusItems.kt new file mode 100644 index 0000000000..c18925e716 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusItems.kt @@ -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, + 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(), + ) + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/onramp/OnrampStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/onramp/OnrampStatusBottomSheet.kt new file mode 100644 index 0000000000..13cfa71096 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/onramp/OnrampStatusBottomSheet.kt @@ -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 \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/onramp/OnrampStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/onramp/OnrampStatusItems.kt new file mode 100644 index 0000000000..0a8a4b0b90 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/onramp/OnrampStatusItems.kt @@ -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, + 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(), + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index ec142bf0ca..9058c501e5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -47,6 +47,8 @@ interface TokenDetailsClickIntents { fun onSwapTransactionClick(txId: String) + fun onOnrampTransactionClick(txId: String) + fun onGoToProviderClick(url: String) fun onSwapPromoDismiss() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index b96f8a62d8..63f8ad3395 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -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 = createSelectedAppCurrencyFlow() private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var stakingEntryInfo: StakingEntryInfo? = null private var stakingAvailability: StakingAvailability = StakingAvailability.Unavailable private var swapTxStatusTaskScheduler = SingleTaskScheduler>() + private var onrampTxStatusTaskScheduler = SingleTaskScheduler>() 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) { 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) }