diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index be887dc4ea..62304b3e12 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -201,7 +201,7 @@
Visit provider’s website to see why
Operation failed by provider
Visit provider’s website for verification
- Provider: Verification needed
+ KYC verification required by provider
Confirmed
Confirming
Exchanged
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 bd69b6feb9..b1c49c1291 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
@@ -10,6 +10,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
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.tokenicon.TokenIcon
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
@@ -39,6 +40,8 @@ fun InputRowApprox(
rightTitle: TextReference,
rightSubtitle: TextReference,
modifier: Modifier = Modifier,
+ leftTitleEllipsisOffset: Int = 0,
+ rightTitleEllipsisOffset: Int = 0,
showDivider: Boolean = false,
) {
DividerContainer(
@@ -47,7 +50,6 @@ fun InputRowApprox(
) {
Row(
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(TangemTheme.dimens.spacing12)
.fillMaxWidth(),
@@ -56,6 +58,7 @@ fun InputRowApprox(
iconState = leftIcon,
title = leftTitle,
subtitle = leftSubtitle,
+ titleEllipsisOffset = leftTitleEllipsisOffset,
modifier = Modifier.weight(1f),
)
Icon(
@@ -64,7 +67,7 @@ fun InputRowApprox(
tint = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(
- horizontal = TangemTheme.dimens.spacing8,
+ horizontal = TangemTheme.dimens.spacing4,
vertical = TangemTheme.dimens.spacing10,
),
)
@@ -72,6 +75,7 @@ fun InputRowApprox(
iconState = rightIcon,
title = rightTitle,
subtitle = rightSubtitle,
+ titleEllipsisOffset = rightTitleEllipsisOffset,
modifier = Modifier.weight(1f),
)
}
@@ -84,6 +88,7 @@ private fun InputRowApproxItem(
title: TextReference,
subtitle: TextReference,
modifier: Modifier = Modifier,
+ titleEllipsisOffset: Int = 0,
) {
Row(
modifier = modifier,
@@ -103,6 +108,7 @@ private fun InputRowApproxItem(
text = title.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
+ ellipsis = TextEllipsis.OffsetEnd(titleEllipsisOffset),
)
EllipsisText(
text = subtitle.resolveReference(),
@@ -122,11 +128,13 @@ private fun InputRowApproxPreview_Light() {
TangemTheme {
InputRowApprox(
leftIcon = TokenIconState.Loading,
- leftTitle = TextReference.Str("Left title"),
- leftSubtitle = TextReference.Str("Left subtitle"),
+ leftTitle = TextReference.Str("Left title USD"),
+ leftSubtitle = TextReference.Str("Left subtitle USD"),
+ leftTitleEllipsisOffset = 3,
rightIcon = TokenIconState.Loading,
- rightTitle = TextReference.Str("Right title Right title Right title Right title Right title"),
- rightSubtitle = TextReference.Str("Right subtitle"),
+ rightTitle = TextReference.Str("Right title Right title Right title Right title Right title USD"),
+ rightSubtitle = TextReference.Str("Right subtitle Right subtitle Right subtitle USD"),
+ rightTitleEllipsisOffset = 3,
modifier = Modifier
.background(TangemTheme.colors.background.action),
)
@@ -139,11 +147,13 @@ private fun InputRowApproxPreview_Dark() {
TangemTheme(isDark = true) {
InputRowApprox(
leftIcon = TokenIconState.Loading,
- leftTitle = TextReference.Str("Left title Left title Left title Left title Left title"),
- leftSubtitle = TextReference.Str("Left subtitle"),
+ leftTitle = TextReference.Str("Left title Left title Left title Left title Left title USD"),
+ leftSubtitle = TextReference.Str("Left subtitle Left subtitle Left subtitle USD"),
+ leftTitleEllipsisOffset = 3,
rightIcon = TokenIconState.Loading,
- rightTitle = TextReference.Str("Right title"),
- rightSubtitle = TextReference.Str("Right subtitle"),
+ rightTitle = TextReference.Str("Right title USD"),
+ rightSubtitle = TextReference.Str("Right subtitle USD"),
+ rightTitleEllipsisOffset = 3,
modifier = Modifier
.background(TangemTheme.colors.background.action),
)
diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt
index d44b32ecdf..766815d328 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt
@@ -104,6 +104,16 @@ fun combinedReference(refs: WrappedList): TextReference {
return TextReference.Combined(refs)
}
+/**
+ * Combines multiple [TextReference] instances into a single [TextReference].
+ *
+ * @param refs Vararg of [TextReference] instances to be combined.
+ * @return A [TextReference] representing the combined text references.
+ */
+fun combinedReference(vararg refs: TextReference): TextReference {
+ return TextReference.Combined(WrappedList(listOf(*refs)))
+}
+
/** Resolve [TextReference] as [String] */
@Composable
@ReadOnlyComposable
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/PeriodicTask.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt
similarity index 96%
rename from features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/PeriodicTask.kt
rename to core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt
index e1343a73cd..5db470b916 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/PeriodicTask.kt
+++ b/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt
@@ -1,4 +1,4 @@
-package com.tangem.feature.swap.viewmodels
+package com.tangem.utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt
index a97c761008..b4e0208243 100644
--- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt
+++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt
@@ -43,6 +43,7 @@ internal class SwapInteractorImpl @Inject constructor(
private val transactionManager: TransactionManager,
private val userWalletManager: UserWalletManager,
private val repository: SwapRepository,
+ private val swapTransactionRepository: SwapTransactionRepository,
private val allowPermissionsHandler: AllowPermissionsHandler,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
@@ -492,6 +493,14 @@ internal class SwapInteractorImpl @Inject constructor(
},
ifRight = {
val timestamp = System.currentTimeMillis()
+ storeSwapTransaction(
+ currencyToSend = currencyToSend,
+ currencyToGet = currencyToGet,
+ amount = amount,
+ swapProvider = swapProvider,
+ swapDataModel = exchangeData.dataModel,
+ timestamp = timestamp,
+ )
TxState.TxSent(
fromAmount = amountFormatter.formatSwapAmountToUI(
amount,
@@ -531,6 +540,28 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
+ private suspend fun storeSwapTransaction(
+ currencyToSend: CryptoCurrencyStatus,
+ currencyToGet: CryptoCurrencyStatus,
+ amount: SwapAmount,
+ swapProvider: SwapProvider,
+ swapDataModel: SwapDataModel,
+ timestamp: Long,
+ ) {
+ swapTransactionRepository.storeTransaction(
+ userWalletId = UserWalletId(userWalletManager.getWalletId()),
+ fromCryptoCurrencyId = currencyToSend.currency.id,
+ toCryptoCurrencyId = currencyToGet.currency.id,
+ transaction = SavedSwapTransactionModel(
+ txId = swapDataModel.transaction.txId,
+ provider = swapProvider,
+ timestamp = timestamp,
+ fromCryptoAmount = amount.value,
+ toCryptoAmount = swapDataModel.toTokenAmount.value,
+ ),
+ )
+ }
+
@Deprecated("used in old swap mechanism")
override fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount {
return SwapAmount(token.value.amount ?: BigDecimal.ZERO, getTokenDecimals(token.currency))
diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt
index e161ee2429..d72ebef25d 100644
--- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt
+++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt
@@ -34,9 +34,10 @@ class SwapDomainModule {
userWalletManager: UserWalletManager,
transactionManager: TransactionManager,
@SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
- @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
+ getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
@SwapScope sendTransactionUseCase: SendTransactionUseCase,
quotesRepository: QuotesRepository,
+ swapTransactionRepository: SwapTransactionRepository,
walletManagersFacade: WalletManagersFacade,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
): SwapInteractor {
@@ -51,6 +52,7 @@ class SwapDomainModule {
quotesRepository = quotesRepository,
walletManagersFacade = walletManagersFacade,
dispatcher = coroutineDispatcherProvider,
+ swapTransactionRepository = swapTransactionRepository,
)
}
@@ -69,7 +71,6 @@ class SwapDomainModule {
return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder)
}
- @SwapScope
@Provides
@Singleton
fun providesGetCryptoCurrencyStatusUseCase(
diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt
index 6b59b50197..f2e6a4d139 100644
--- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt
+++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt
@@ -1,5 +1,7 @@
package com.tangem.feature.swap.domain.models.domain
+import java.math.BigDecimal
+
data class SavedSwapTransactionListModel(
val userWalletId: String,
val fromCryptoCurrencyId: String,
@@ -10,10 +12,8 @@ data class SavedSwapTransactionListModel(
data class SavedSwapTransactionModel(
val txId: String,
val timestamp: Long,
- val fromCryptoAmount: String,
- val toCryptoAmount: String,
+ val fromCryptoAmount: BigDecimal,
+ val toCryptoAmount: BigDecimal,
val provider: SwapProvider,
- val toFiatAmount: String? = null,
- val fromFiatAmount: String? = null,
val status: ExchangeStatusModel? = null,
)
\ No newline at end of file
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt
index 814723462c..09d8728073 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt
@@ -8,6 +8,7 @@ data class SwapSuccessStateHolder(
val txUrl: String,
val fee: TextReference,
val rate: TextReference,
+ val showStatusButton: Boolean,
val providerName: TextReference,
val providerType: TextReference,
val providerIcon: String,
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt
index 0b18d866ad..b81bbb59e4 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt
@@ -14,6 +14,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.converters.TokensDataConverter
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.SwapAmount
+import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
@@ -592,6 +593,7 @@ internal class StateBuilder(
txUrl = txUrl,
providerName = TextReference.Str(providerState.name),
providerType = TextReference.Str(providerState.type),
+ showStatusButton = providerState.type == ExchangeProviderType.CEX.name,
providerIcon = providerState.iconUrl,
fee = TextReference.Str("${fee.amountCrypto} ${fee.symbolCrypto} (${fee.amountFiatFormatted})"),
rate = TextReference.Str(providerState.rate),
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt
index ffb9334830..4c4221d250 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt
@@ -43,6 +43,7 @@ fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) {
SwapSuccessScreenButtons(
textRes = R.string.common_close,
txUrl = state.txUrl,
+ showStatusButton = state.showStatusButton,
onExploreClick = state.onSecondaryButtonClick,
onDoneClick = onBack,
)
@@ -105,6 +106,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad
private fun SwapSuccessScreenButtons(
@StringRes textRes: Int,
txUrl: String,
+ showStatusButton: Boolean,
onExploreClick: () -> Unit,
onDoneClick: () -> Unit,
) {
@@ -119,21 +121,23 @@ private fun SwapSuccessScreenButtons(
if (txUrl.isNotBlank()) {
Row {
SecondaryButtonIconStart(
- text = stringResource(id = com.tangem.core.ui.R.string.common_explore),
- iconResId = com.tangem.core.ui.R.drawable.ic_web_24,
+ text = stringResource(id = R.string.common_explore),
+ iconResId = R.drawable.ic_web_24,
onClick = onExploreClick,
modifier = Modifier.weight(1f),
)
- SpacerW12()
- SecondaryButtonIconStart(
- text = stringResource(id = com.tangem.core.ui.R.string.common_share),
- iconResId = com.tangem.core.ui.R.drawable.ic_share_24,
- onClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- context.shareText(txUrl)
- },
- modifier = Modifier.weight(1f),
- )
+ if (showStatusButton) {
+ SpacerW12()
+ SecondaryButtonIconStart(
+ text = stringResource(id = R.string.express_cex_status_button_title),
+ iconResId = R.drawable.ic_arrow_top_right_24,
+ onClick = {
+ hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
+ context.shareText(txUrl)
+ },
+ modifier = Modifier.weight(1f),
+ )
+ }
}
SpacerH12()
}
@@ -154,6 +158,7 @@ private val state = SwapSuccessStateHolder(
fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"),
providerName = TextReference.Str("1inch"),
providerType = TextReference.Str(ExchangeProviderType.DEX.name),
+ showStatusButton = false,
providerIcon = "",
fromTokenAmount = TextReference.Str("1 000 DAI"),
toTokenAmount = TextReference.Str("1 000 MATIC"),
diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts
index c19e3f3577..d7592edba4 100644
--- a/features/tokendetails/impl/build.gradle.kts
+++ b/features/tokendetails/impl/build.gradle.kts
@@ -37,6 +37,7 @@ dependencies {
implementation(deps.tangem.blockchain)
implementation(deps.tangem.card.core)
implementation(deps.timber)
+ implementation(deps.lifecycle.compose)
/** DI */
implementation(deps.hilt.android)
@@ -66,6 +67,9 @@ dependencies {
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
+ /** Temp dependency to swap domain */
+ implementation(projects.features.swap.domain)
+
/** Feature Apis */
implementation(projects.features.tokendetails.api)
implementation(projects.features.send.api)
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 9941c1bbda..353bc30d6d 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
@@ -1,24 +1,28 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.domain.tokens.model.CryptoCurrency
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.components.ExchangeStatusNotifications
-import kotlinx.collections.immutable.PersistentList
+import kotlinx.coroutines.flow.MutableStateFlow
internal data class SwapTransactionsState(
val txId: String,
val provider: SwapProvider,
val txUrl: String? = null,
- val timestamp: Long,
- val statuses: PersistentList,
- val activeStatus: ExchangeStatus?,
+ val timestamp: TextReference,
val fiatSymbol: String,
- val notification: ExchangeStatusNotifications? = null,
+ val activeStatus: MutableStateFlow,
+ val statuses: MutableStateFlow>,
+ val notification: MutableStateFlow = MutableStateFlow(null),
+ val toCryptoCurrencyId: CryptoCurrency.ID,
val toCryptoAmount: String,
val toCryptoSymbol: String,
val toFiatAmount: String,
val toCurrencyIcon: TokenIconState,
+ val fromCryptoCurrencyId: CryptoCurrency.ID,
val fromCryptoAmount: String,
val fromCryptoSymbol: String,
val fromFiatAmount: String,
@@ -29,6 +33,7 @@ internal data class SwapTransactionsState(
internal class ExchangeStatusState(
val status: ExchangeStatus,
+ val text: TextReference,
val isActive: Boolean,
val isDone: Boolean,
)
\ No newline at end of file
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 1494ed003f..368164ce46 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
@@ -27,6 +27,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.viewmodels.TokenDetailsClickIntents
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
@@ -242,6 +243,19 @@ internal class TokenDetailsStateFactory(
return state.copy(notifications = notificationConverter.removeRentInfo(state))
}
+ fun getStateWithExchangeStatusBottomSheet(txId: String): TokenDetailsState {
+ val state = currentStateProvider()
+ return state.copy(
+ bottomSheetConfig = TangemBottomSheetConfig(
+ isShow = true,
+ onDismissRequest = clickIntents::onDismissBottomSheet,
+ content = ExchangeStatusBottomSheetConfig(
+ value = state.swapTxs.first { it.txId == txId },
+ ),
+ ),
+ )
+ }
+
fun getStateAndTriggerEvent(
state: TokenDetailsState,
errorMessage: TextReference,
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
new file mode 100644
index 0000000000..c0be53eb94
--- /dev/null
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt
@@ -0,0 +1,227 @@
+package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
+
+import com.tangem.common.Provider
+import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.extensions.combinedReference
+import com.tangem.core.ui.utils.BigDecimalFormatter
+import com.tangem.core.ui.utils.toDateFormat
+import com.tangem.core.ui.utils.toTimeFormat
+import com.tangem.domain.appcurrency.model.AppCurrency
+import com.tangem.domain.tokens.model.CryptoCurrencyStatus
+import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
+import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
+import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState
+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.viewmodels.TokenDetailsClickIntents
+import com.tangem.features.tokendetails.impl.R
+import com.tangem.utils.converter.Converter
+import kotlinx.collections.immutable.PersistentList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toPersistentList
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import java.math.BigDecimal
+
+internal class TokenDetailsSwapTransactionsStateConverter(
+ private val clickIntents: TokenDetailsClickIntents,
+ appCurrencyProvider: Provider,
+) : Converter> {
+
+ private val iconStateConverter = CryptoCurrencyToIconStateConverter()
+ private val appCurrency = appCurrencyProvider()
+
+ override fun convert(value: Unit): PersistentList {
+ return persistentListOf()
+ }
+
+ fun convert(
+ savedTransactions: List,
+ cryptoStatusList: List,
+ ): PersistentList {
+ val result = mutableListOf()
+
+ savedTransactions.forEach { swapCurrency ->
+ val firstStatus = cryptoStatusList.first { it.currency.id.value == swapCurrency.fromCryptoCurrencyId }
+ val secondStatus = cryptoStatusList.first { it.currency.id.value == swapCurrency.toCryptoCurrencyId }
+
+ val (fromCurrency, toCurrency) = if (swapCurrency.fromCryptoCurrencyId == firstStatus.currency.id.value) {
+ firstStatus to secondStatus
+ } else {
+ secondStatus to firstStatus
+ }
+
+ swapCurrency.transactions.forEach { transaction ->
+ val toAmount = transaction.toCryptoAmount
+ val fromAmount = transaction.fromCryptoAmount
+ val toFiatAmount = toAmount.multiply(toCurrency.value.fiatRate)
+ val fromFiatAmount = fromAmount.multiply(fromCurrency.value.fiatRate)
+ val timestamp = transaction.timestamp
+ result.add(
+ SwapTransactionsState(
+ txId = transaction.txId,
+ provider = transaction.provider,
+ txUrl = transaction.status?.txUrl,
+ timestamp = TextReference.Str("${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}"),
+ fiatSymbol = appCurrency.symbol,
+ statuses = MutableStateFlow(getStatuses(transaction.status?.status)),
+ activeStatus = MutableStateFlow(transaction.status?.status),
+ notification = MutableStateFlow(
+ getNotification(
+ transaction.status?.status,
+ transaction.status?.txUrl,
+ ),
+ ),
+ toCryptoCurrencyId = toCurrency.currency.id,
+ toCryptoAmount = BigDecimalFormatter.formatCryptoAmount(
+ cryptoAmount = toAmount,
+ cryptoCurrency = toCurrency.currency,
+ ),
+ toCryptoSymbol = toCurrency.currency.symbol,
+ toFiatAmount = getFiatAmount(toFiatAmount),
+ toCurrencyIcon = iconStateConverter.convert(toCurrency),
+ fromCryptoCurrencyId = fromCurrency.currency.id,
+ fromCryptoAmount = BigDecimalFormatter.formatCryptoAmount(
+ cryptoAmount = fromAmount,
+ cryptoCurrency = fromCurrency.currency,
+ ),
+ fromCryptoSymbol = fromCurrency.currency.symbol,
+ fromFiatAmount = getFiatAmount(fromFiatAmount),
+ fromCurrencyIcon = iconStateConverter.convert(fromCurrency),
+ onClick = { clickIntents.onSwapTransactionClick(transaction.txId) },
+ onGoToProviderClick = { clickIntents.onGoToProviderClick(transaction.status?.txUrl.orEmpty()) },
+ ),
+ )
+ }
+ }
+ return result.toPersistentList()
+ }
+
+ fun updateTxStatus(tx: SwapTransactionsState, status: ExchangeStatus?) {
+ if (tx.activeStatus.value == status) return
+ tx.activeStatus.update { status }
+ tx.notification.update { getNotification(status, tx.txUrl) }
+ tx.statuses.update { getStatuses(status) }
+ }
+
+ private fun getFiatAmount(toFiatAmount: BigDecimal): String {
+ return BigDecimalFormatter.formatFiatAmount(
+ fiatAmount = toFiatAmount,
+ fiatCurrencyCode = appCurrency.code,
+ fiatCurrencySymbol = appCurrency.symbol,
+ )
+ }
+
+ private fun getNotification(status: ExchangeStatus?, txUrl: String?): ExchangeStatusNotifications? {
+ if (txUrl == null) return null
+ return when (status) {
+ ExchangeStatus.Failed -> ExchangeStatusNotifications.Failed {
+ clickIntents.onGoToProviderClick(txUrl)
+ }
+ ExchangeStatus.Verifying -> ExchangeStatusNotifications.NeedVerification {
+ clickIntents.onGoToProviderClick(txUrl)
+ }
+ else -> null
+ }
+ }
+
+ private fun getStatuses(status: ExchangeStatus?): List {
+ val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting
+ val isConfirming = status == ExchangeStatus.Confirming
+ val isVerifying = status == ExchangeStatus.Verifying
+ val isExchanging = status == ExchangeStatus.Exchanging
+ val isFailed = status == ExchangeStatus.Failed
+ val isSending = status == ExchangeStatus.Sending
+
+ val isWaitingDone = !isWaiting
+ val isConfirmingDone = !isConfirming && isWaitingDone
+ val isExchangingDone = !isExchanging && isConfirmingDone
+ val isSendingDone = !isSending && !isVerifying && !isFailed && isExchangingDone
+
+ return listOf(
+ waitStep(isWaiting, isWaitingDone),
+ confirmStep(isConfirming, isConfirmingDone),
+ exchangeStep(isExchanging, isExchangingDone, isVerifying, isFailed),
+ sendStep(isSending, isSendingDone),
+ )
+ }
+
+ private fun waitStep(isNew: Boolean, isNewDone: Boolean) = ExchangeStatusState(
+ status = ExchangeStatus.New,
+ text = when {
+ isNew -> combinedReference(
+ TextReference.Res(R.string.express_exchange_status_receiving),
+ TextReference.Str(STATUS_ACTIVE_DOTS),
+ )
+ else -> TextReference.Res(R.string.express_exchange_status_receiving)
+ },
+ isActive = isNew,
+ isDone = isNewDone,
+ )
+
+ private fun confirmStep(isConfirming: Boolean, isConfirmingDone: Boolean) = ExchangeStatusState(
+ status = ExchangeStatus.Confirming,
+ text = when {
+ isConfirming -> combinedReference(
+ TextReference.Res(R.string.express_exchange_status_confirming),
+ TextReference.Str(STATUS_ACTIVE_DOTS),
+ )
+ isConfirmingDone -> TextReference.Res(R.string.express_exchange_status_confirmed)
+ else -> TextReference.Res(R.string.express_exchange_status_confirming)
+ },
+ isActive = isConfirming,
+ isDone = isConfirmingDone,
+ )
+
+ private fun exchangeStep(
+ isExchanging: Boolean,
+ isExchangingDone: Boolean,
+ isVerifying: Boolean = false,
+ isFailed: Boolean = false,
+ ) = when {
+ isVerifying -> ExchangeStatusState(
+ status = ExchangeStatus.Verifying,
+ text = TextReference.Res(R.string.express_exchange_status_verifying),
+ isActive = true,
+ isDone = false,
+ )
+ isFailed -> ExchangeStatusState(
+ status = ExchangeStatus.Failed,
+ text = TextReference.Res(R.string.express_exchange_status_failed),
+ isActive = true,
+ isDone = false,
+ )
+ else -> ExchangeStatusState(
+ status = ExchangeStatus.Exchanging,
+ text = when {
+ isExchanging -> combinedReference(
+ TextReference.Res(R.string.express_exchange_status_exchanging),
+ TextReference.Str(STATUS_ACTIVE_DOTS),
+ )
+ isExchangingDone -> TextReference.Res(R.string.express_exchange_status_exchanged)
+ else -> TextReference.Res(R.string.express_exchange_status_exchanging)
+ },
+ isActive = isExchanging,
+ isDone = isExchangingDone,
+ )
+ }
+
+ private fun sendStep(isSending: Boolean, isSendingDone: Boolean) = ExchangeStatusState(
+ status = ExchangeStatus.Sending,
+ text = when {
+ isSending -> combinedReference(
+ TextReference.Res(R.string.express_exchange_status_sending),
+ TextReference.Str(STATUS_ACTIVE_DOTS),
+ )
+ isSendingDone -> TextReference.Res(R.string.express_exchange_status_sent)
+ else -> TextReference.Res(R.string.express_exchange_status_sending)
+ },
+ isActive = isSending,
+ isDone = isSendingDone,
+ )
+
+ private companion object {
+ private const val STATUS_ACTIVE_DOTS = "…"
+ }
+}
\ No newline at end of file
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/exchange/ExchangeEstimate.kt
index c33b4fc290..6aee05217a 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/exchange/ExchangeEstimate.kt
@@ -22,7 +22,9 @@ internal fun ExchangeEstimate(
fromTokenIconState: TokenIconState,
toTokenIconState: TokenIconState,
fromCryptoAmount: TextReference,
+ fromCryptoSymbol: String,
toCryptoAmount: TextReference,
+ toCryptoSymbol: String,
fromFiatAmount: TextReference,
toFiatAmount: TextReference,
modifier: Modifier = Modifier,
@@ -59,9 +61,11 @@ internal fun ExchangeEstimate(
leftIcon = fromTokenIconState,
leftTitle = fromCryptoAmount,
leftSubtitle = fromFiatAmount,
+ leftTitleEllipsisOffset = fromCryptoSymbol.length,
rightIcon = toTokenIconState,
rightTitle = toCryptoAmount,
rightSubtitle = toFiatAmount,
+ rightTitleEllipsisOffset = toCryptoSymbol.length,
)
}
}
\ No newline at end of file
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/exchange/ExchangeStatusBlock.kt
index b9d7dd77ee..909849ae07 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/exchange/ExchangeStatusBlock.kt
@@ -17,19 +17,22 @@ 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.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.SpacerWMax
+import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState
import com.tangem.features.tokendetails.impl.R
-import kotlinx.collections.immutable.PersistentList
+import kotlinx.coroutines.flow.MutableStateFlow
@Composable
internal fun ExchangeStatusBlock(
- statuses: PersistentList,
+ statuses: MutableStateFlow>,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
+ val statusValues = statuses.collectAsStateWithLifecycle()
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
@@ -70,10 +73,10 @@ internal fun ExchangeStatusBlock(
}
}
- statuses.forEachIndexed { index, item ->
+ statusValues.value.forEachIndexed { index, item ->
ExchangeStatusStep(
stepStatus = item,
- isLast = index == statuses.lastIndex,
+ isLast = index == statusValues.value.lastIndex,
)
}
}
@@ -128,7 +131,7 @@ private fun ExchangeStatusStepText(stepStatus: ExchangeStatusState) {
}
Text(
- text = getStatusText(stepStatus)?.let { stringResource(it) }.orEmpty(),
+ text = stepStatus.text.resolveReference(),
style = TangemTheme.typography.body2,
color = textColor,
modifier = Modifier
@@ -206,34 +209,4 @@ private fun ExchangeStepSeparator() {
shape = CircleShape,
),
)
-}
-
-private fun getStatusText(stepStatus: ExchangeStatusState) = when (stepStatus.status) {
- ExchangeStatus.Failed -> R.string.express_exchange_status_failed
- ExchangeStatus.Verifying -> if (stepStatus.isDone) {
- R.string.express_exchange_status_verified
- } else {
- R.string.express_exchange_status_verifying
- }
- ExchangeStatus.New, ExchangeStatus.Waiting -> if (stepStatus.isDone) {
- R.string.express_exchange_status_received
- } else {
- R.string.express_exchange_status_receiving
- }
- ExchangeStatus.Confirming -> if (stepStatus.isDone) {
- R.string.express_exchange_status_confirmed
- } else {
- R.string.express_exchange_status_confirming
- }
- ExchangeStatus.Exchanging -> if (stepStatus.isDone) {
- R.string.express_exchange_status_exchanged
- } else {
- R.string.express_exchange_status_exchanging
- }
- ExchangeStatus.Sending -> if (stepStatus.isDone) {
- R.string.express_exchange_status_sent
- } else {
- R.string.express_exchange_status_sending
- }
- else -> null
}
\ No newline at end of file
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/exchange/ExchangeStatusBottomSheet.kt
index b32c69f48e..f02ca3d0a7 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/exchange/ExchangeStatusBottomSheet.kt
@@ -8,6 +8,7 @@ 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.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12
@@ -19,8 +20,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
-import com.tangem.core.ui.utils.toDateFormat
-import com.tangem.core.ui.utils.toTimeFormat
+import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
@Composable
@@ -36,6 +36,8 @@ internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) {
@Composable
private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetConfig) {
val config = content.value
+ val status = config.activeStatus.collectAsStateWithLifecycle()
+ val notification = config.notification.collectAsStateWithLifecycle()
Column(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
@@ -55,13 +57,14 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC
.align(CenterHorizontally),
)
SpacerH16()
- val timestamp = config.timestamp
ExchangeEstimate(
- timestamp = TextReference.Str("${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}"),
+ timestamp = config.timestamp,
fromTokenIconState = config.fromCurrencyIcon,
toTokenIconState = config.toCurrencyIcon,
fromCryptoAmount = TextReference.Str(config.fromCryptoAmount),
+ fromCryptoSymbol = config.fromCryptoSymbol,
toCryptoAmount = TextReference.Str(config.toCryptoAmount),
+ toCryptoSymbol = config.toCryptoSymbol,
fromFiatAmount = TextReference.Str(config.fromFiatAmount),
toFiatAmount = TextReference.Str(config.toFiatAmount),
)
@@ -77,12 +80,20 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC
onClick = config.onGoToProviderClick,
)
AnimatedContent(
- targetState = config.notification,
+ targetState = notification.value,
label = "Exchange Status Notification Change",
) {
it?.let {
- SpacerH12()
- Notification(config = it.config)
+ val tint = when (status.value) {
+ ExchangeStatus.Verifying -> TangemTheme.colors.icon.attention
+ ExchangeStatus.Failed -> TangemTheme.colors.icon.warning
+ else -> null
+ }
+ Notification(
+ config = it.config,
+ iconTint = tint,
+ modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
+ )
}
}
SpacerH24()
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/exchange/ExchangeStatusItems.kt
index 4ea9a31b08..79c9ab8392 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/exchange/ExchangeStatusItems.kt
@@ -4,9 +4,7 @@ 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.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@@ -16,9 +14,11 @@ 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.constraintlayout.compose.ConstraintLayout
-import androidx.constraintlayout.compose.Dimension
-import androidx.constraintlayout.compose.Visibility
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.constraintlayout.compose.*
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.tokenicon.TokenIcon
@@ -41,8 +41,8 @@ internal fun LazyListScope.swapTransactionsItems(
contentType = { swapTxs[it]::class.java },
) {
val item = swapTxs[it]
-
- val (iconRes, tint) = when (item.activeStatus) {
+ val status = item.activeStatus.collectAsStateWithLifecycle()
+ val (iconRes, tint) = when (status.value) {
ExchangeStatus.Verifying -> R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention
ExchangeStatus.Failed -> R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning
else -> null to null
@@ -54,7 +54,6 @@ internal fun LazyListScope.swapTransactionsItems(
toTokenIconState = item.toCurrencyIcon,
fromAmount = item.fromCryptoAmount,
fromSymbol = item.fromCryptoSymbol,
- toAmount = item.toCryptoAmount,
toSymbol = item.toCryptoSymbol,
onClick = item.onClick,
infoIconRes = iconRes,
@@ -73,7 +72,6 @@ private fun ExchangeStatusItem(
toTokenIconState: TokenIconState,
fromAmount: String,
fromSymbol: String,
- toAmount: String,
toSymbol: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
@@ -121,7 +119,7 @@ private fun ExchangeStatusItem(
top.linkTo(titleRef.bottom, padding6)
end.linkTo(swapIconRef.start)
bottom.linkTo(parent.bottom)
- width = Dimension.fillToConstraints
+ width = Dimension.fillToConstraints.atMostWrapContent
},
)
Icon(
@@ -149,17 +147,16 @@ private fun ExchangeStatusItem(
bottom.linkTo(parent.bottom)
},
)
- EllipsisText(
- text = toAmount,
+ Text(
+ text = toSymbol,
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)
- end.linkTo(infoIconRef.start, padding6)
+ end.linkTo(infoIconRef.start, padding6, padding6)
bottom.linkTo(parent.bottom)
- width = Dimension.fillToConstraints
+ width = Dimension.fillToConstraints.atLeastWrapContent
},
)
Icon(
@@ -192,4 +189,54 @@ private fun ExchangeStatusItem(
},
)
}
-}
\ No newline at end of file
+}
+
+//region Preview
+@Preview
+@Composable
+private fun ExchangeStatusItemPreview_Light(
+ @PreviewParameter(ExchangeStatusItemsPreviewParameterProvider::class) amount: String,
+) {
+ TangemTheme {
+ ExchangeStatusItem(
+ providerName = "ChangeNow",
+ fromTokenIconState = TokenIconState.Loading,
+ toTokenIconState = TokenIconState.Loading,
+ fromAmount = amount,
+ fromSymbol = "USDT",
+ toSymbol = "USDT",
+ onClick = {},
+ infoIconRes = null,
+ infoIconTint = null,
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun ExchangeStatusItemPreview_Dark(
+ @PreviewParameter(ExchangeStatusItemsPreviewParameterProvider::class) amount: String,
+) {
+ TangemTheme(isDark = true) {
+ ExchangeStatusItem(
+ providerName = "ChangeNow",
+ fromTokenIconState = TokenIconState.Loading,
+ toTokenIconState = TokenIconState.Loading,
+ fromAmount = amount,
+ fromSymbol = "USDT",
+ toSymbol = "USDT",
+ onClick = {},
+ infoIconRes = null,
+ infoIconTint = null,
+ )
+ }
+}
+
+private class ExchangeStatusItemsPreviewParameterProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ "1111111111111111111111111111 USDT",
+ "11111 USDT",
+ )
+}
+//endregion
\ No newline at end of file
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/viewmodels/ExchangeStatusFactory.kt
new file mode 100644
index 0000000000..d92306d03c
--- /dev/null
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt
@@ -0,0 +1,141 @@
+package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels
+
+import arrow.core.getOrElse
+import com.tangem.common.Provider
+import com.tangem.domain.appcurrency.model.AppCurrency
+import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
+import com.tangem.domain.tokens.model.CryptoCurrency
+import com.tangem.domain.tokens.model.CryptoCurrencyStatus
+import com.tangem.domain.wallets.models.UserWalletId
+import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
+import com.tangem.feature.swap.domain.SwapRepository
+import com.tangem.feature.swap.domain.SwapTransactionRepository
+import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
+import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel
+import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
+import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
+import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter
+import com.tangem.utils.coroutines.CoroutineDispatcherProvider
+import kotlinx.collections.immutable.PersistentList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toPersistentList
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.withContext
+
+@Suppress("LongParameterList")
+internal class ExchangeStatusFactory(
+ private val swapTransactionRepository: SwapTransactionRepository,
+ private val swapRepository: SwapRepository,
+ private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
+ private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
+ private val dispatchers: CoroutineDispatcherProvider,
+ private val clickIntents: TokenDetailsClickIntents,
+ private val appCurrencyProvider: Provider,
+ private val userWalletId: UserWalletId,
+ private val cryptoCurrencyId: CryptoCurrency.ID,
+) {
+
+ private val swapTransactionsStateConverter by lazy {
+ TokenDetailsSwapTransactionsStateConverter(
+ clickIntents = clickIntents,
+ appCurrencyProvider = appCurrencyProvider,
+ )
+ }
+
+ operator fun invoke() = combine(
+ flow = swapTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId),
+ flow2 = getWalletCryptoCurrencies().conflate(),
+ ) { savedTransactions, cryptoCurrenciesStatusList ->
+ innerLoadSwapState(
+ savedTransactions,
+ cryptoCurrenciesStatusList,
+ )
+ }
+
+ private fun getWalletCryptoCurrencies() = flow {
+ val selectedWallet = getSelectedWalletSyncUseCase().fold(
+ ifLeft = { null },
+ ifRight = { it },
+ )
+ requireNotNull(selectedWallet) { "No selected wallet" }
+
+ val cryptoCurrenciesList = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId)
+ .getOrElse { emptyList() }
+
+ emit(cryptoCurrenciesList)
+ }
+
+ suspend fun updateSwapTxStatuses(swapTxList: PersistentList) = withContext(dispatchers.io) {
+ swapTxList.map { tx ->
+ async {
+ val status = getExchangeStatus(tx.txId)?.status
+ swapTransactionsStateConverter.updateTxStatus(tx, status)
+ tx.removeIfFinished(status)
+ }
+ }
+ .awaitAll()
+ .filterNotNull()
+ .toPersistentList()
+ }
+
+ private suspend fun innerLoadSwapState(
+ savedTransactions: List?,
+ cryptoCurrenciesStatusList: List,
+ ): PersistentList {
+ val txWithStatuses = savedTransactions?.map { currencySwaps ->
+ currencySwaps.copy(
+ transactions = currencySwaps.transactions.map { tx ->
+ val status = getExchangeStatus(tx.txId)
+ tx.copy(
+ status = status,
+ )
+ },
+ )
+ }
+
+ return getExchangeStatusState(
+ savedTransactions = txWithStatuses,
+ cryptoCurrencyStatusList = cryptoCurrenciesStatusList,
+ )
+ }
+
+ private suspend fun getExchangeStatus(txId: String): ExchangeStatusModel? {
+ return swapRepository.getExchangeStatus(txId)
+ .fold(
+ ifLeft = { null },
+ ifRight = { it },
+ )
+ }
+
+ private fun getExchangeStatusState(
+ savedTransactions: List?,
+ cryptoCurrencyStatusList: List,
+ ): PersistentList {
+ if (savedTransactions == null || cryptoCurrencyStatusList.isEmpty()) {
+ return persistentListOf()
+ }
+
+ return swapTransactionsStateConverter.convert(
+ savedTransactions = savedTransactions,
+ cryptoStatusList = cryptoCurrencyStatusList,
+ )
+ }
+
+ private suspend fun SwapTransactionsState.removeIfFinished(status: ExchangeStatus?): SwapTransactionsState? {
+ return if (status == ExchangeStatus.Refunded || status == ExchangeStatus.Finished) {
+ swapTransactionRepository.removeTransaction(
+ userWalletId = userWalletId,
+ fromCryptoCurrencyId = fromCryptoCurrencyId,
+ toCryptoCurrencyId = toCryptoCurrencyId,
+ txId = txId,
+ )
+ null
+ } else {
+ this
+ }
+ }
+}
\ 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 992915899c..95093f7fdb 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
@@ -38,4 +38,8 @@ interface TokenDetailsClickIntents {
fun onDismissBottomSheet()
fun onCloseRentInfoNotification()
+
+ fun onSwapTransactionClick(txId: String)
+
+ fun onGoToProviderClick(url: String)
}
\ No newline at end of file
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 55cb6c48be..126298c471 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
@@ -31,16 +31,19 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
+import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
+import com.tangem.feature.swap.domain.SwapRepository
+import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
+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.TokenDetailsStateFactory
import com.tangem.features.tokendetails.impl.R
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
-import com.tangem.utils.coroutines.CoroutineDispatcherProvider
-import com.tangem.utils.coroutines.JobHolder
-import com.tangem.utils.coroutines.saveIn
+import com.tangem.utils.coroutines.*
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.collections.immutable.PersistentList
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.*
@@ -50,7 +53,7 @@ import java.math.BigDecimal
import javax.inject.Inject
import kotlin.properties.Delegates
-@Suppress("LongParameterList", "LargeClass")
+@Suppress("LongParameterList", "LargeClass", "TooManyFunctions")
@HiltViewModel
internal class TokenDetailsViewModel @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
@@ -67,6 +70,10 @@ internal class TokenDetailsViewModel @Inject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
+ private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
+ private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
+ private val swapRepository: SwapRepository,
+ private val swapTransactionRepository: SwapTransactionRepository,
private val walletManagersFacade: WalletManagersFacade,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val reduxStateHolder: ReduxStateHolder,
@@ -86,8 +93,11 @@ internal class TokenDetailsViewModel @Inject constructor(
private val marketPriceJobHolder = JobHolder()
private val refreshStateJobHolder = JobHolder()
private val warningsJobHolder = JobHolder()
+ private val swapTxJobHolder = JobHolder()
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
+ private var swapTxStatusTaskScheduler = SingleTaskScheduler>()
+
private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow()
private val stateFactory = TokenDetailsStateFactory(
@@ -98,6 +108,20 @@ internal class TokenDetailsViewModel @Inject constructor(
decimals = cryptoCurrency.decimals,
)
+ private val exchangeStatusFactory by lazy {
+ ExchangeStatusFactory(
+ swapTransactionRepository = swapTransactionRepository,
+ swapRepository = swapRepository,
+ getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
+ getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
+ dispatchers = dispatchers,
+ clickIntents = this,
+ appCurrencyProvider = Provider { selectedAppCurrencyFlow.value },
+ userWalletId = userWalletId,
+ cryptoCurrencyId = cryptoCurrency.id,
+ )
+ }
+
var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(cryptoCurrency))
private set
@@ -109,8 +133,14 @@ internal class TokenDetailsViewModel @Inject constructor(
handleBalanceHiding(owner)
}
+ override fun onCleared() {
+ swapTxStatusTaskScheduler.cancelTask()
+ super.onCleared()
+ }
+
private fun updateContent() {
subscribeOnCurrencyStatusUpdates()
+ subscribeOnExchangeTransactionsUpdates()
updateTxHistory(refresh = false, showItemsLoading = true)
}
@@ -177,6 +207,33 @@ internal class TokenDetailsViewModel @Inject constructor(
}
}
+ private fun subscribeOnExchangeTransactionsUpdates() {
+ viewModelScope.launch(dispatchers.io) {
+ swapTxStatusTaskScheduler.cancelTask()
+ exchangeStatusFactory.invoke()
+ .onEach { swapTxs ->
+ swapTxStatusTaskScheduler.scheduleTask(
+ viewModelScope,
+ PeriodicTask(
+ delay = EXCHANGE_STATUS_UPDATE_DELAY,
+ task = {
+ runCatching(dispatchers.io) {
+ exchangeStatusFactory.updateSwapTxStatuses(swapTxs)
+ }
+ },
+ onSuccess = { updatedTxs ->
+ uiState = uiState.copy(swapTxs = updatedTxs)
+ },
+ onError = {},
+ ),
+ )
+ }
+ .flowOn(dispatchers.io)
+ .launchIn(viewModelScope)
+ .saveIn(swapTxJobHolder)
+ }
+ }
+
/**
* @param refresh - invalidate cache and get data from remote
* @param showItemsLoading - show loading items placeholder.
@@ -475,35 +532,16 @@ internal class TokenDetailsViewModel @Inject constructor(
override fun onCloseRentInfoNotification() {
uiState = stateFactory.getStateWithRemovedRentNotification()
}
-}
-//
-// val text = when (stepStatus.status) {
-// ExchangeStatus.Failed -> stringResource(id = R.string.express_exchange_status_failed)
-// ExchangeStatus.Verifying -> if (stepStatus.isDone) {
-// stringResource(id = R.string.express_exchange_status_verified)
-// } else {
-// stringResource(id = R.string.express_exchange_status_verifying)
-// }
-// ExchangeStatus.New, ExchangeStatus.Waiting -> if (stepStatus.isDone) {
-// stringResource(id = R.string.express_exchange_status_received)
-// } else {
-// stringResource(id = R.string.express_exchange_status_receiving)
-// }
-// ExchangeStatus.Confirming -> if (stepStatus.isDone) {
-// stringResource(id = R.string.express_exchange_status_confirmed)
-// } else {
-// stringResource(id = R.string.express_exchange_status_confirming)
-// }
-// ExchangeStatus.Exchanging -> if (stepStatus.isDone) {
-// stringResource(id = R.string.express_exchange_status_exchanged)
-// } else {
-// stringResource(id = R.string.express_exchange_status_exchanging)
-// }
-// ExchangeStatus.Sending -> if (stepStatus.isDone) {
-// stringResource(id = R.string.express_exchange_status_sent)
-// } else {
-// stringResource(id = R.string.express_exchange_status_sending)
-// }
-// else -> ""
-// }
\ No newline at end of file
+ override fun onSwapTransactionClick(txId: String) {
+ uiState = stateFactory.getStateWithExchangeStatusBottomSheet(txId)
+ }
+
+ override fun onGoToProviderClick(url: String) {
+ router.openUrl(url)
+ }
+
+ private companion object {
+ const val EXCHANGE_STATUS_UPDATE_DELAY = 10_000L
+ }
+}
\ No newline at end of file