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