Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-26 10:31:08 +05:00
commit 94af682f08
27 changed files with 583 additions and 593 deletions

View file

@ -54,9 +54,14 @@ internal object OnrampDomainModule {
@Singleton
fun provideGetOnrampStatusUseCase(
onrampRepository: OnrampRepository,
onrampTransactionRepository: OnrampTransactionRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampStatusUseCase {
return GetOnrampStatusUseCase(onrampRepository, onrampErrorResolver)
return GetOnrampStatusUseCase(
onrampRepository,
onrampTransactionRepository,
onrampErrorResolver,
)
}
@Provides

View file

@ -1,8 +1,9 @@
package com.tangem.common.ui.expressStatus
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.ExpressNotificationsUM
@ -12,21 +13,20 @@ import com.tangem.core.ui.res.TangemTheme
@Composable
fun ExpressStatusNotificationBlock(state: NotificationUM?) {
AnimatedContent(
targetState = state,
AnimatedVisibility(
visible = state?.config != null,
modifier = Modifier.padding(top = 12.dp),
label = "Express Status Notification Change",
) { notification ->
if (notification?.config != null) {
Notification(
config = notification.config,
iconTint = when (state) {
is ExpressNotificationsUM.NeedVerification -> TangemTheme.colors.icon.attention
is ExpressNotificationsUM.FailedByProvider -> TangemTheme.colors.icon.warning
else -> null
},
containerColor = TangemTheme.colors.background.action,
)
}
) {
val wrappedNotification = remember(this) { requireNotNull(state?.config) }
Notification(
config = wrappedNotification,
iconTint = when (state) {
is ExpressNotificationsUM.NeedVerification -> TangemTheme.colors.icon.attention
is ExpressNotificationsUM.FailedByProvider -> TangemTheme.colors.icon.warning
else -> null
},
containerColor = TangemTheme.colors.background.action,
)
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSet
import com.tangem.datasource.local.preferences.utils.getObjectSetSync
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.tokens.model.CryptoCurrency
@ -62,6 +63,12 @@ internal class DefaultOnrampTransactionRepository(
}.map(transactionConverter::convert)
}
override suspend fun updateTransactionStatus(txId: String, status: OnrampStatus.Status) =
withContext(dispatchers.io) {
val updatedTx = getTransactionById(txId)?.copy(status = status) ?: return@withContext
storeTransaction(updatedTx)
}
override suspend fun removeTransaction(txId: String) {
withContext(dispatchers.io) {
appPreferencesStore.editData { mutablePreferences ->

View file

@ -5,15 +5,19 @@ import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
class GetOnrampStatusUseCase(
private val onrampRepository: OnrampRepository,
private val onrampTransactionRepository: OnrampTransactionRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(txId: String): Either<OnrampError, OnrampStatus> {
return Either.catch {
onrampRepository.getStatus(txId)
val status = onrampRepository.getStatus(txId)
onrampTransactionRepository.updateTransactionStatus(txId = txId, status = status.status)
status
}.mapLeft {
errorResolver.resolve(it)
}

View file

@ -1,12 +1,17 @@
package com.tangem.domain.onramp
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEmpty
class GetOnrampTransactionsUseCase(
private val onrampTransactionRepository: OnrampTransactionRepository,
@ -15,14 +20,12 @@ class GetOnrampTransactionsUseCase(
operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): Either<OnrampError, Flow<List<OnrampTransaction>>> {
return Either.catch {
onrampTransactionRepository.getTransactions(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrencyId,
)
}.mapLeft {
OnrampError.UnknownError
}
): Flow<Either<OnrampError, List<OnrampTransaction>>> {
return onrampTransactionRepository.getTransactions(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrencyId,
).map { it.right() }
.catch { OnrampError.UnknownError.left() }
.onEmpty { OnrampError.UnknownError.left() }
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.onramp.repositories
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
@ -13,5 +14,7 @@ interface OnrampTransactionRepository {
fun getTransactions(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow<List<OnrampTransaction>>
suspend fun updateTransactionStatus(txId: String, status: OnrampStatus.Status)
suspend fun removeTransaction(txId: String)
}

View file

@ -307,8 +307,7 @@ internal object TokenDetailsPreviewData {
),
dialogConfig = null,
pendingTxs = persistentListOf(),
swapTxs = persistentListOf(),
onrampTxs = persistentListOf(),
expressTxs = persistentListOf(),
pullToRefreshConfig = pullToRefreshConfig,
bottomSheetConfig = null,
isBalanceHidden = false,
@ -338,8 +337,7 @@ internal object TokenDetailsPreviewData {
),
dialogConfig = null,
pendingTxs = persistentListOf(),
swapTxs = persistentListOf(),
onrampTxs = persistentListOf(),
expressTxs = persistentListOf(),
pullToRefreshConfig = pullToRefreshConfig,
bottomSheetConfig = null,
isBalanceHidden = false,

View file

@ -1,43 +0,0 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
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.ImmutableList
internal data class SwapTransactionsState(
val txId: String,
val provider: SwapProvider,
val txUrl: String? = null,
val txExternalId: String? = null,
val timestamp: TextReference,
val fiatSymbol: String,
val activeStatus: ExchangeStatus?,
val hasFailed: Boolean,
val statuses: ImmutableList<ExchangeStatusState>,
val notification: ExchangeStatusNotifications? = null,
val toCryptoCurrency: CryptoCurrency,
val toCryptoAmount: String,
val toFiatAmount: String,
val toCurrencyIcon: CurrencyIconState,
val fromCryptoCurrency: CryptoCurrency,
val fromCryptoAmount: TextReference,
val fromFiatAmount: String,
val fromCurrencyIcon: CurrencyIconState,
val showProviderLink: Boolean,
val isRefundTerminalStatus: Boolean = true,
val onClick: () -> Unit,
val onGoToProviderClick: (String) -> Unit,
)
@Immutable
internal data class ExchangeStatusState(
val status: ExchangeStatus,
val text: TextReference,
val isActive: Boolean,
val isDone: Boolean,
)

View file

@ -21,8 +21,7 @@ internal data class TokenDetailsState(
val stakingBlocksState: StakingBlockUM?,
val notifications: ImmutableList<TokenDetailsNotification>,
val pendingTxs: PersistentList<TransactionState>,
val swapTxs: PersistentList<SwapTransactionsState>,
val onrampTxs: PersistentList<ExpressTransactionStateUM.OnrampUM>,
val expressTxs: PersistentList<ExpressTransactionStateUM>,
val txHistoryState: TxHistoryState,
val dialogConfig: TokenDetailsDialogConfig?,
val pullToRefreshConfig: PullToRefreshConfig,

View file

@ -0,0 +1,14 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.express
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import javax.annotation.concurrent.Immutable
@Deprecated("Use ExpressStatusBlock from common")
@Immutable
internal data class ExchangeStatusState(
val status: ExchangeStatus,
val text: TextReference,
val isActive: Boolean,
val isDone: Boolean,
)

View file

@ -5,9 +5,9 @@ import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.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.ExchangeStatusState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications
import kotlinx.collections.immutable.ImmutableList
@ -23,6 +23,8 @@ internal sealed class ExpressTransactionStateUM {
val notification: ExchangeStatusNotifications? = null,
val showProviderLink: Boolean,
val isRefundTerminalStatus: Boolean,
val fromCryptoCurrency: CryptoCurrency,
val toCryptoCurrency: CryptoCurrency,
) : ExpressTransactionStateUM()
data class OnrampUM(
@ -30,20 +32,22 @@ internal sealed class ExpressTransactionStateUM {
val providerName: String, // todo onramp fix after SwapProvider moved to own module
val providerImageUrl: String, // todo onramp fix after SwapProvider moved to own module
val providerType: String, // todo onramp fix after SwapProvider moved to own module
val cryptoCurrencyName: String,
val activeStatus: OnrampStatus.Status,
) : ExpressTransactionStateUM()
}
internal data class ExpressTransactionStateInfoUM(
val title: TextReference,
val status: ExpressStatusUM,
val notification: NotificationUM?,
val txId: String,
val txUrl: String?,
val txExternalId: String?,
val timestamp: TextReference,
val txExternalUrl: String?,
val timestamp: Long,
val timestampFormatted: TextReference,
val onGoToProviderClick: (String) -> Unit,
val onClick: () -> Unit,
val iconState: ExpressTransactionStateIconUM,
val toAmount: TextReference,
val toFiatAmount: TextReference?,
@ -54,4 +58,10 @@ internal data class ExpressTransactionStateInfoUM(
val fromFiatAmount: TextReference?,
val fromAmountSymbol: String,
val fromCurrencyIcon: CurrencyIconState,
)
)
internal enum class ExpressTransactionStateIconUM {
Warning,
Error,
None,
}

View file

@ -14,13 +14,14 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateIconUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateInfoUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
@ -43,15 +44,17 @@ internal class TokenDetailsOnrampTransactionStateConverter(
val appCurrency = appCurrencyProvider()
return ExpressTransactionStateUM.OnrampUM(
info = ExpressTransactionStateInfoUM(
title = resourceReference(id = R.string.express_status_buying, wrappedList(cryptoCurrency.name)),
status = convertStatuses(value.status, value.externalTxUrl),
notification = getNotification(value.status, value.externalTxUrl),
txId = value.txId,
txExternalId = value.externalTxId,
txUrl = value.externalTxUrl,
timestamp = resourceReference(
txExternalUrl = value.externalTxUrl,
timestamp = value.timestamp,
timestampFormatted = resourceReference(
R.string.send_date_format,
wrappedList(
value.timestamp.toTimeFormat(DateTimeFormatters.dateFormatter),
value.timestamp.toDateFormatWithTodayYesterday(),
value.timestamp.toTimeFormat(),
),
),
@ -82,13 +85,13 @@ internal class TokenDetailsOnrampTransactionStateConverter(
url = value.fromCurrency.image,
fallbackResId = R.drawable.ic_currency_24,
),
iconState = getIconState(value.status),
onGoToProviderClick = clickIntents::onGoToProviderClick,
onClick = { clickIntents.onOnrampTransactionClick(value.txId) },
onClick = { clickIntents.onExpressTransactionClick(value.txId) },
),
providerName = value.providerName,
providerImageUrl = value.providerImageUrl,
providerType = value.providerType,
cryptoCurrencyName = cryptoCurrency.name,
activeStatus = value.status,
)
}
@ -110,6 +113,14 @@ internal class TokenDetailsOnrampTransactionStateConverter(
}
}
private fun getIconState(status: OnrampStatus.Status): ExpressTransactionStateIconUM {
return when (status) {
OnrampStatus.Status.Verifying -> ExpressTransactionStateIconUM.Warning
OnrampStatus.Status.Failed -> ExpressTransactionStateIconUM.Error
else -> ExpressTransactionStateIconUM.None
}
}
private fun convertStatuses(status: OnrampStatus.Status, externalTxUrl: String?): ExpressStatusUM {
val statuses = with(status) {
persistentListOf(

View file

@ -66,8 +66,7 @@ internal class TokenDetailsSkeletonStateConverter(
stakingBlocksState = StakingBlockUM.Loading(iconState).takeIf { isSupportedInMobileApp },
notifications = persistentListOf(),
pendingTxs = persistentListOf(),
swapTxs = persistentListOf(),
onrampTxs = persistentListOf(),
expressTxs = persistentListOf(),
txHistoryState = TxHistoryState.Content(
contentItems = MutableStateFlow(
value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick),

View file

@ -28,7 +28,6 @@ import com.tangem.domain.txhistory.models.TxHistoryListError
import com.tangem.domain.txhistory.models.TxHistoryStateError
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
@ -36,7 +35,6 @@ 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.express.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
@ -301,31 +299,6 @@ internal class TokenDetailsStateFactory(
return state.copy(notifications = notificationConverter.removeHederaAssociateWarning(state))
}
fun getStateWithExchangeStatusBottomSheet(swapTxState: SwapTransactionsState): TokenDetailsState {
return currentStateProvider().copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissBottomSheet,
content = ExchangeStatusBottomSheetConfig(
value = swapTxState,
),
),
)
}
fun updateStateWithExchangeStatusBottomSheet(swapTxState: SwapTransactionsState): TangemBottomSheetConfig? {
val state = currentStateProvider()
val bottomSheetConfig = state.bottomSheetConfig
val currentConfig = bottomSheetConfig?.content as? ExchangeStatusBottomSheetConfig ?: return bottomSheetConfig
return bottomSheetConfig.copy(
content = if (currentConfig.value != swapTxState) {
ExchangeStatusBottomSheetConfig(swapTxState)
} else {
currentConfig
},
)
}
fun getStateAndTriggerEvent(
state: TokenDetailsState,
errorMessage: TextReference,

View file

@ -1,13 +1,16 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.domain.appcurrency.model.AppCurrency
@ -17,9 +20,12 @@ import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
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.ExchangeStatusState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeStatusState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateIconUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateInfoUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
@ -32,25 +38,27 @@ import timber.log.Timber
import java.math.BigDecimal
import java.util.Locale
// Fixme [REDACTED_JIRA]
@Suppress("LargeClass")
internal class TokenDetailsSwapTransactionsStateConverter(
private val clickIntents: TokenDetailsClickIntents,
private val cryptoCurrency: CryptoCurrency,
private val analyticsEventsHandlerProvider: Provider<AnalyticsEventHandler>,
appCurrencyProvider: Provider<AppCurrency>,
) : Converter<Unit, PersistentList<SwapTransactionsState>> {
) : Converter<Unit, PersistentList<ExpressTransactionStateUM.ExchangeUM>> {
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
private val appCurrency = appCurrencyProvider()
override fun convert(value: Unit): PersistentList<SwapTransactionsState> {
override fun convert(value: Unit): PersistentList<ExpressTransactionStateUM.ExchangeUM> {
return persistentListOf()
}
fun convert(
savedTransactions: List<SavedSwapTransactionListModel>,
quotes: Set<Quote>,
): PersistentList<SwapTransactionsState> {
val result = mutableListOf<SwapTransactionsState>()
): PersistentList<ExpressTransactionStateUM.ExchangeUM> {
val result = mutableListOf<ExpressTransactionStateUM.ExchangeUM>()
savedTransactions
.forEach { swapTransaction ->
@ -72,42 +80,26 @@ internal class TokenDetailsSwapTransactionsStateConverter(
fromFiatAmount = quote.fiatRate.multiply(fromAmount)
}
}
val timestamp = transaction.timestamp
val notifications =
getNotification(transaction.status?.status, transaction.status?.txExternalUrl, null)
val showProviderLink = getShowProviderLink(notifications, transaction.status)
result.add(
SwapTransactionsState(
txId = transaction.txId,
ExpressTransactionStateUM.ExchangeUM(
provider = transaction.provider,
txUrl = transaction.status?.txExternalUrl,
txExternalId = transaction.status?.txExternalId,
timestamp = TextReference.Str(
"${timestamp.toDateFormatWithTodayYesterday()}, ${timestamp.toTimeFormat()}",
),
fiatSymbol = appCurrency.symbol,
statuses = getStatuses(transaction.status?.status),
hasFailed = transaction.status?.status == ExchangeStatus.Failed,
activeStatus = transaction.status?.status,
notification = notifications,
toCryptoCurrency = toCryptoCurrency,
toCryptoAmount = toAmount.format { crypto(toCryptoCurrency) },
toFiatAmount = getFiatAmount(toFiatAmount),
toCurrencyIcon = iconStateConverter.convert(toCryptoCurrency),
fromCryptoCurrency = fromCryptoCurrency,
fromCryptoAmount = stringReference(
fromAmount.format { crypto(fromCryptoCurrency) },
),
fromFiatAmount = getFiatAmount(fromFiatAmount),
fromCurrencyIcon = iconStateConverter.convert(fromCryptoCurrency),
activeStatus = transaction.status?.status,
showProviderLink = showProviderLink,
onClick = { clickIntents.onSwapTransactionClick(transaction.txId) },
onGoToProviderClick = { url ->
analyticsEventsHandlerProvider().send(
TokenExchangeAnalyticsEvent.GoToProviderStatus(cryptoCurrency.symbol),
)
clickIntents.onGoToProviderClick(url = url)
},
isRefundTerminalStatus = true,
fromCryptoCurrency = fromCryptoCurrency,
toCryptoCurrency = toCryptoCurrency,
info = createStateInfo(
transaction,
toCryptoCurrency,
fromCryptoCurrency,
toFiatAmount,
fromFiatAmount,
),
),
)
}
@ -116,37 +108,79 @@ internal class TokenDetailsSwapTransactionsStateConverter(
}
fun updateTxStatus(
tx: SwapTransactionsState,
tx: ExpressTransactionStateUM.ExchangeUM,
statusModel: ExchangeStatusModel?,
refundToken: CryptoCurrency?,
isRefundTerminalStatus: Boolean,
): SwapTransactionsState {
): ExpressTransactionStateUM.ExchangeUM {
if (statusModel == null || tx.activeStatus == statusModel.status) {
Timber.e("UpdateTxStatus isn't required. Current status isn't changed")
return tx
}
val hasFailed = tx.hasFailed || statusModel.status == ExchangeStatus.Failed
val hasFailed = statusModel.status == ExchangeStatus.Failed
val notifications = getNotification(statusModel.status, statusModel.txExternalUrl, refundToken)
val showProviderLink = getShowProviderLink(notifications, statusModel)
return tx.copy(
activeStatus = statusModel.status,
hasFailed = hasFailed,
notification = notifications,
statuses = getStatuses(statusModel.status, hasFailed),
txUrl = statusModel.txExternalUrl,
showProviderLink = showProviderLink,
isRefundTerminalStatus = isRefundTerminalStatus,
info = tx.info.copy(txExternalUrl = statusModel.txExternalUrl),
)
}
private fun getFiatAmount(toFiatAmount: BigDecimal?): String {
return BigDecimalFormatter.formatFiatAmount(
fiatAmount = toFiatAmount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
private fun createStateInfo(
transaction: SavedSwapTransactionModel,
toCryptoCurrency: CryptoCurrency,
fromCryptoCurrency: CryptoCurrency,
toFiatAmount: BigDecimal?,
fromFiatAmount: BigDecimal?,
): ExpressTransactionStateInfoUM {
val timestamp = transaction.timestamp
return ExpressTransactionStateInfoUM(
title = resourceReference(R.string.express_exchange_by, wrappedList(transaction.provider.name)),
txId = transaction.txId,
txExternalUrl = transaction.status?.txExternalUrl,
txExternalId = transaction.status?.txExternalId,
timestamp = timestamp,
timestampFormatted = stringReference(
"${timestamp.toDateFormatWithTodayYesterday()}, ${timestamp.toTimeFormat()}",
),
toAmount = getCryptoAmount(transaction.toCryptoAmount, toCryptoCurrency),
toFiatAmount = getFiatAmount(toFiatAmount),
toCurrencyIcon = iconStateConverter.convert(toCryptoCurrency),
toAmountSymbol = toCryptoCurrency.symbol,
fromAmount = getCryptoAmount(transaction.fromCryptoAmount, fromCryptoCurrency),
fromFiatAmount = getFiatAmount(fromFiatAmount),
fromCurrencyIcon = iconStateConverter.convert(fromCryptoCurrency),
fromAmountSymbol = fromCryptoCurrency.symbol,
onClick = { clickIntents.onExpressTransactionClick(transaction.txId) },
onGoToProviderClick = { url ->
analyticsEventsHandlerProvider().send(
TokenExchangeAnalyticsEvent.GoToProviderStatus(cryptoCurrency.symbol),
)
clickIntents.onGoToProviderClick(url = url)
},
iconState = getIconState(transaction.status?.status),
status = getStatusState(),
notification = null, // fixme [REDACTED_JIRA]
)
}
private fun getCryptoAmount(amount: BigDecimal?, cryptoCurrency: CryptoCurrency) = stringReference(
amount.format { crypto(cryptoCurrency) },
)
private fun getFiatAmount(toFiatAmount: BigDecimal?) = stringReference(
toFiatAmount.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
)
private fun getNotification(
status: ExchangeStatus?,
txUrl: String?,
@ -186,6 +220,21 @@ internal class TokenDetailsSwapTransactionsStateConverter(
}
}
private fun getIconState(status: ExchangeStatus?): ExpressTransactionStateIconUM {
return when (status) {
ExchangeStatus.Verifying -> ExpressTransactionStateIconUM.Warning
ExchangeStatus.Failed, ExchangeStatus.Cancelled -> ExpressTransactionStateIconUM.Error
else -> ExpressTransactionStateIconUM.None
}
}
// Fixme [REDACTED_JIRA]
private fun getStatusState() = ExpressStatusUM(
title = resourceReference(R.string.express_exchange_status_title),
link = ExpressLinkUM.Empty,
statuses = persistentListOf(),
)
private fun getShowProviderLink(notifications: ExchangeStatusNotifications?, status: ExchangeStatusModel?) =
notifications == null && status?.txExternalUrl != null && status.status != ExchangeStatus.Cancelled

View file

@ -14,39 +14,36 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
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.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
@Suppress("LongParameterList")
internal class ExchangeStatusFactory(
internal class ExchangeStatusFactory @AssistedInject constructor(
private val swapTransactionRepository: SwapTransactionRepository,
private val swapRepository: SwapRepository,
private val quotesRepository: QuotesRepository,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val swapTransactionStatusStore: SwapTransactionStatusStore,
private val dispatchers: CoroutineDispatcherProvider,
private val clickIntents: TokenDetailsClickIntents,
private val appCurrencyProvider: Provider<AppCurrency>,
private val analyticsEventsHandlerProvider: Provider<AnalyticsEventHandler>,
private val currentStateProvider: Provider<TokenDetailsState>,
private val userWalletId: UserWalletId,
private val cryptoCurrency: CryptoCurrency,
@Assisted private val clickIntents: TokenDetailsClickIntents,
@Assisted private val appCurrencyProvider: Provider<AppCurrency>,
@Assisted private val analyticsEventsHandlerProvider: Provider<AnalyticsEventHandler>,
@Assisted private val currentStateProvider: Provider<TokenDetailsState>,
@Assisted private val userWalletId: UserWalletId,
@Assisted private val cryptoCurrency: CryptoCurrency,
) {
private val swapTransactionsStateConverter by lazy {
@ -58,7 +55,7 @@ internal class ExchangeStatusFactory(
)
}
suspend operator fun invoke(): Flow<PersistentList<SwapTransactionsState>> {
suspend operator fun invoke(): Flow<PersistentList<ExpressTransactionStateUM.ExchangeUM>> {
val selectedWallet = getSelectedWalletSyncUseCase().fold(
ifLeft = { return emptyFlow() },
ifRight = { it },
@ -82,50 +79,39 @@ internal class ExchangeStatusFactory(
}
}
suspend fun removeTransactionOnBottomSheetClosed(isForceTerminal: Boolean = false): TokenDetailsState {
suspend fun removeTransactionOnBottomSheetClosed(isForceTerminal: Boolean = false) {
val state = currentStateProvider()
val bottomSheetConfig = state.bottomSheetConfig?.content as? ExchangeStatusBottomSheetConfig ?: return state
val selectedTx = bottomSheetConfig.value
val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return
val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.ExchangeUM ?: return
val shouldTerminate = selectedTx.activeStatus.isTerminal(selectedTx.isRefundTerminalStatus) || isForceTerminal
return if (shouldTerminate) {
if (shouldTerminate) {
swapTransactionRepository.removeTransaction(
userWalletId = userWalletId,
fromCryptoCurrency = selectedTx.fromCryptoCurrency,
toCryptoCurrency = selectedTx.toCryptoCurrency,
txId = selectedTx.txId,
txId = selectedTx.info.txId,
)
val filteredTxs = state.swapTxs
.filterNot { it.txId == selectedTx.txId }
.toPersistentList()
state.copy(swapTxs = filteredTxs)
} else {
state
}
}
suspend fun updateSwapTxStatuses(swapTxList: PersistentList<SwapTransactionsState>) = withContext(dispatchers.io) {
swapTxList.map { tx ->
async {
val statusModel = getExchangeStatus(tx.txId, tx.provider)
val isRefundTerminalStatus = statusModel?.refundNetwork == null &&
statusModel?.refundContractAddress == null &&
tx.provider.type != ExchangeProviderType.DEX_BRIDGE
if (tx.activeStatus.isTerminal(isRefundTerminalStatus)) {
tx
} else {
val addedRefundToken = addRefundCurrencyIfNeeded(statusModel, tx.provider.type)
swapTransactionsStateConverter.updateTxStatus(
tx = tx,
statusModel = statusModel,
refundToken = addedRefundToken,
isRefundTerminalStatus = isRefundTerminalStatus,
)
}
}
suspend fun updateSwapTxStatus(swapTx: ExpressTransactionStateUM.ExchangeUM): ExpressTransactionStateUM.ExchangeUM {
return if (swapTx.activeStatus.isTerminal(swapTx.isRefundTerminalStatus)) {
swapTx
} else {
val statusModel = getExchangeStatus(swapTx.info.txId, swapTx.provider)
val isRefundTerminalStatus = statusModel?.refundNetwork == null &&
statusModel?.refundContractAddress == null &&
swapTx.provider.type != ExchangeProviderType.DEX_BRIDGE
val addedRefundToken = addRefundCurrencyIfNeeded(statusModel, swapTx.provider.type)
swapTransactionsStateConverter.updateTxStatus(
tx = swapTx,
statusModel = statusModel,
refundToken = addedRefundToken,
isRefundTerminalStatus = isRefundTerminalStatus,
)
}
.awaitAll()
.toPersistentList()
}
private suspend fun getExchangeStatus(txId: String, provider: SwapProvider): ExchangeStatusModel? {
@ -177,7 +163,7 @@ internal class ExchangeStatusFactory(
private fun getExchangeStatusState(
savedTransactions: List<SavedSwapTransactionListModel>?,
quotes: Set<Quote>,
): PersistentList<SwapTransactionsState> {
): PersistentList<ExpressTransactionStateUM.ExchangeUM> {
if (savedTransactions == null) {
return persistentListOf()
}
@ -224,4 +210,16 @@ internal class ExchangeStatusFactory(
emptySet()
}
}
@AssistedFactory
interface Factory {
fun create(
clickIntents: TokenDetailsClickIntents,
appCurrencyProvider: Provider<AppCurrency>,
analyticsEventsHandlerProvider: Provider<AnalyticsEventHandler>,
currentStateProvider: Provider<TokenDetailsState>,
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): ExchangeStatusFactory
}
}

View file

@ -0,0 +1,162 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
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.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.withContext
@Suppress("LongParameterList")
internal class ExpressStatusFactory @AssistedInject constructor(
@Assisted private val currentStateProvider: Provider<TokenDetailsState>,
@Assisted private val clickIntents: TokenDetailsClickIntents,
@Assisted private val cryptoCurrency: CryptoCurrency,
@Assisted appCurrencyProvider: Provider<AppCurrency>,
@Assisted analyticsEventsHandlerProvider: Provider<AnalyticsEventHandler>,
@Assisted userWalletId: UserWalletId,
@Assisted cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventsHandler: AnalyticsEventHandler,
onrampStatusFactory: OnrampStatusFactory.Factory,
exchangeStatusFactory: ExchangeStatusFactory.Factory,
) {
private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
exchangeStatusFactory.create(
clickIntents = clickIntents,
appCurrencyProvider = appCurrencyProvider,
analyticsEventsHandlerProvider = analyticsEventsHandlerProvider,
currentStateProvider = currentStateProvider,
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
)
}
private val onrampStatusFactory by lazy(LazyThreadSafetyMode.NONE) {
onrampStatusFactory.create(
currentStateProvider = currentStateProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
appCurrencyProvider = appCurrencyProvider,
clickIntents = clickIntents,
cryptoCurrency = cryptoCurrency,
userWalletId = userWalletId,
)
}
suspend fun getExpressStatuses(): Flow<PersistentList<ExpressTransactionStateUM>> = combine(
flow = exchangeStatusFactory(),
flow2 = onrampStatusFactory(),
) { maybeExchange, maybeOnramp ->
persistentListOf(
maybeOnramp,
maybeExchange,
).flatten()
.sortedByDescending { it.info.timestamp }
.toPersistentList()
}
suspend fun getUpdatedExpressStatuses(expressTxs: PersistentList<ExpressTransactionStateUM>) =
withContext(dispatchers.io) {
expressTxs.map { tx ->
async {
when (tx) {
is ExpressTransactionStateUM.ExchangeUM -> exchangeStatusFactory.updateSwapTxStatus(tx)
is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.updateOnrmapTxStatus(tx)
}
}
}.awaitAll()
.toPersistentList()
}
fun getStateWithUpdatedExpressTxs(
expressTxs: PersistentList<ExpressTransactionStateUM>,
updateBalance: (CryptoCurrency) -> Unit,
): TokenDetailsState {
val state = currentStateProvider()
val config = state.bottomSheetConfig
val expressBottomSheet = config?.content as? ExpressStatusBottomSheetConfig
val currentTx = expressTxs.firstOrNull { it.info.txId == expressBottomSheet?.value?.info?.txId }
if (currentTx is ExpressTransactionStateUM.ExchangeUM && currentTx.activeStatus == ExchangeStatus.Finished) {
updateBalance(currentTx.toCryptoCurrency)
}
return state.copy(
expressTxs = expressTxs,
bottomSheetConfig = currentTx?.let(
::updateStateWithExpressStatusBottomSheet,
) ?: config,
)
}
fun getStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): TokenDetailsState {
analyticsEventsHandler.send(TokenExchangeAnalyticsEvent.CexTxStatusOpened(cryptoCurrency.symbol))
return currentStateProvider().copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissBottomSheet,
content = ExpressStatusBottomSheetConfig(
value = expressState,
),
),
)
}
fun updateStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): TangemBottomSheetConfig? {
val state = currentStateProvider()
val bottomSheetConfig = state.bottomSheetConfig
val currentConfig = bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return bottomSheetConfig
return bottomSheetConfig.copy(
content = if (currentConfig.value != expressState) {
ExpressStatusBottomSheetConfig(expressState)
} else {
currentConfig
},
)
}
suspend fun removeTransactionOnBottomSheetClosed(
expressState: ExpressTransactionStateUM,
isForceTerminal: Boolean = false,
) {
when (expressState) {
is ExpressTransactionStateUM.ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(
isForceTerminal,
)
is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.removeTransactionOnBottomSheetClosed()
}
}
@AssistedFactory
interface Factory {
@Suppress("LongParameterList")
fun create(
clickIntents: TokenDetailsClickIntents,
appCurrencyProvider: Provider<AppCurrency>,
analyticsEventsHandlerProvider: Provider<AnalyticsEventHandler>,
currentStateProvider: Provider<TokenDetailsState>,
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
): ExpressStatusFactory
}
}

View file

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

View file

@ -48,12 +48,9 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange.ExchangeStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange.swapTransactionsItems
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp.OnrampStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp.OnrampStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp.onrampTransactionsItems
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.expressTransactionsItems
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
@ -180,13 +177,8 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon
)
}
swapTransactionsItems(
swapTxs = state.swapTxs,
modifier = itemModifier,
)
onrampTransactionsItems(
onrampTxs = state.onrampTxs,
expressTransactionsItems(
expressTxs = state.expressTxs,
modifier = itemModifier,
)
@ -214,11 +206,8 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon
is ChooseAddressBottomSheetConfig -> {
ChooseAddressBottomSheet(config = config)
}
is ExchangeStatusBottomSheetConfig -> {
ExchangeStatusBottomSheet(config = config)
}
is OnrampStatusBottomSheetConfig -> {
OnrampStatusBottomSheet(config = config)
is ExpressStatusBottomSheetConfig -> {
ExpressStatusBottomSheet(config = config)
}
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
import androidx.compose.runtime.Composable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange.ExchangeStatusBottomSheetContent
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp.OnrampStatusBottomSheetContent
internal data class ExpressStatusBottomSheetConfig(
val value: ExpressTransactionStateUM,
) : TangemBottomSheetConfigContent
@Composable
internal fun ExpressStatusBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
containerColor = TangemTheme.colors.background.tertiary,
) { content: ExpressStatusBottomSheetConfig ->
when (val state = content.value) {
is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(state)
is ExpressTransactionStateUM.ExchangeUM -> ExchangeStatusBottomSheetContent(state)
}
}
}

View file

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

View file

@ -25,7 +25,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeStatusState
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList

View file

@ -14,30 +14,17 @@ import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.notifications.CurrencyNotification
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressEstimate
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressProvider
@Composable
internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
containerColor = TangemTheme.colors.background.tertiary,
) { content: ExchangeStatusBottomSheetConfig ->
ExchangeStatusBottomSheetContent(config = content.value)
}
}
@Composable
private fun ExchangeStatusBottomSheetContent(config: SwapTransactionsState) {
internal fun ExchangeStatusBottomSheetContent(state: ExpressTransactionStateUM.ExchangeUM) {
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) {
SpacerH10()
Text(
@ -57,31 +44,31 @@ private fun ExchangeStatusBottomSheetContent(config: SwapTransactionsState) {
)
SpacerH16()
ExpressEstimate(
timestamp = config.timestamp,
fromTokenIconState = config.fromCurrencyIcon,
toTokenIconState = config.toCurrencyIcon,
fromCryptoAmount = config.fromCryptoAmount,
fromCryptoSymbol = config.fromCryptoCurrency.symbol,
toCryptoAmount = TextReference.Str(config.toCryptoAmount),
toCryptoSymbol = config.toCryptoCurrency.symbol,
fromFiatAmount = TextReference.Str(config.fromFiatAmount),
toFiatAmount = TextReference.Str(config.toFiatAmount),
timestamp = state.info.timestampFormatted,
fromTokenIconState = state.info.fromCurrencyIcon,
toTokenIconState = state.info.toCurrencyIcon,
fromCryptoAmount = state.info.fromAmount,
fromCryptoSymbol = state.info.fromAmountSymbol,
toCryptoAmount = state.info.toAmount,
toCryptoSymbol = state.info.toAmountSymbol,
fromFiatAmount = state.info.fromFiatAmount,
toFiatAmount = state.info.toFiatAmount,
)
SpacerH12()
ExpressProvider(
providerName = TextReference.Str(config.provider.name),
providerType = TextReference.Str(config.provider.type.providerName),
providerTxId = config.txExternalId,
imageUrl = config.provider.imageLarge,
providerName = TextReference.Str(state.provider.name),
providerType = TextReference.Str(state.provider.type.providerName),
providerTxId = state.info.txExternalId,
imageUrl = state.provider.imageLarge,
)
SpacerH12()
ExchangeStatusBlock(
statuses = config.statuses,
showLink = config.showProviderLink,
onClick = { config.onGoToProviderClick(config.txUrl.orEmpty()) },
statuses = state.statuses,
showLink = state.showProviderLink,
onClick = { state.info.onGoToProviderClick(state.info.txExternalUrl.orEmpty()) },
)
if (config.notification != null) {
Notification(state = config.notification, activeStatus = config.activeStatus)
if (state.notification != null) {
Notification(state = state.notification, activeStatus = state.activeStatus)
}
SpacerH24()
}
@ -114,8 +101,4 @@ private fun Notification(state: ExchangeStatusNotifications, activeStatus: Excha
}
}
}
}
internal data class ExchangeStatusBottomSheetConfig(
val value: SwapTransactionsState,
) : TangemBottomSheetConfigContent
}

View file

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

View file

@ -15,9 +15,6 @@ import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
@ -25,17 +22,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.e
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressProvider
@Composable
internal fun OnrampStatusBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
containerColor = TangemTheme.colors.background.tertiary,
) { content: OnrampStatusBottomSheetConfig ->
OnrampStatusBottomSheetContent(config = content.value)
}
}
@Composable
private fun OnrampStatusBottomSheetContent(config: ExpressTransactionStateUM.OnrampUM) {
internal fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) {
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) {
SpacerH10()
Text(
@ -55,33 +42,27 @@ private fun OnrampStatusBottomSheetContent(config: ExpressTransactionStateUM.Onr
)
SpacerH16()
ExpressEstimate(
timestamp = config.info.timestamp,
fromTokenIconState = config.info.fromCurrencyIcon,
toTokenIconState = config.info.toCurrencyIcon,
fromCryptoAmount = config.info.fromAmount,
fromCryptoSymbol = config.info.fromAmountSymbol,
toCryptoAmount = config.info.toAmount,
toCryptoSymbol = config.info.toAmountSymbol,
fromFiatAmount = config.info.fromFiatAmount,
toFiatAmount = config.info.toFiatAmount,
timestamp = state.info.timestampFormatted,
fromTokenIconState = state.info.fromCurrencyIcon,
toTokenIconState = state.info.toCurrencyIcon,
fromCryptoAmount = state.info.fromAmount,
fromCryptoSymbol = state.info.fromAmountSymbol,
toCryptoAmount = state.info.toAmount,
toCryptoSymbol = state.info.toAmountSymbol,
fromFiatAmount = state.info.fromFiatAmount,
toFiatAmount = state.info.toFiatAmount,
)
SpacerH12()
ExpressProvider(
providerName = stringReference(config.providerName),
providerType = stringReference(config.providerType),
providerTxId = config.info.txExternalId,
imageUrl = config.providerImageUrl,
providerName = stringReference(state.providerName),
providerType = stringReference(state.providerType),
providerTxId = state.info.txExternalId,
imageUrl = state.providerImageUrl,
)
SpacerH12()
ExpressStatusBlock(state = config.info.status)
if (config.info.notification != null) {
ExpressStatusNotificationBlock(state = config.info.notification)
}
ExpressStatusBlock(state = state.info.status)
ExpressStatusNotificationBlock(state = state.info.notification)
SpacerH24()
}
}
internal data class OnrampStatusBottomSheetConfig(
val value: ExpressTransactionStateUM.OnrampUM,
) : TangemBottomSheetConfigContent
}

View file

@ -45,9 +45,7 @@ interface TokenDetailsClickIntents {
fun onCloseRentInfoNotification()
fun onSwapTransactionClick(txId: String)
fun onOnrampTransactionClick(txId: String)
fun onExpressTransactionClick(txId: String)
fun onGoToProviderClick(url: String)

View file

@ -22,7 +22,6 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
@ -30,11 +29,6 @@ import com.tangem.domain.card.GetExtendedPublicKeyForCurrencyUseCase
import com.tangem.domain.card.NetworkHasDerivationUseCase
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.onramp.GetOnrampStatusUseCase
import com.tangem.domain.onramp.GetOnrampTransactionsUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.onramp.OnrampSaveTransactionUseCase
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
@ -50,12 +44,10 @@ import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.transaction.error.AssociateAssetError
import com.tangem.domain.transaction.usecase.AssociateAssetUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
@ -64,22 +56,16 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
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.SwapTransactionRepository
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsCurrencyStatusAnalyticsSender
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExchangeStatusFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.OnrampStatusFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp.OnrampStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExpressStatusFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
@ -111,8 +97,6 @@ internal class TokenDetailsViewModel @Inject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val shouldShowSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase,
@ -120,20 +104,13 @@ internal class TokenDetailsViewModel @Inject constructor(
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
private val getYieldUseCase: GetYieldUseCase,
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
private val swapRepository: SwapRepository,
private val swapTransactionRepository: SwapTransactionRepository,
private val quotesRepository: QuotesRepository,
private val swapTransactionStatusStore: SwapTransactionStatusStore,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val associateAssetUseCase: AssociateAssetUseCase,
private val reduxStateHolder: ReduxStateHolder,
private val analyticsEventsHandler: AnalyticsEventHandler,
private val vibratorHapticManager: VibratorHapticManager,
private val clipboardManager: ClipboardManager,
private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase,
private val onrampSaveTransactionUseCase: OnrampSaveTransactionUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
private val getOnrampStatusUseCase: GetOnrampStatusUseCase,
expressStatusFactory: ExpressStatusFactory.Factory,
getUserWalletUseCase: GetUserWalletUseCase,
getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase,
deepLinksRegistry: DeepLinksRegistry,
@ -157,15 +134,13 @@ internal class TokenDetailsViewModel @Inject constructor(
private val marketPriceJobHolder = JobHolder()
private val refreshStateJobHolder = JobHolder()
private val warningsJobHolder = JobHolder()
private val swapTxJobHolder = JobHolder()
private val onrampTxJobHolder = JobHolder()
private val expressTxJobHolder = JobHolder()
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
private var stakingEntryInfo: StakingEntryInfo? = null
private var stakingAvailability: StakingAvailability = StakingAvailability.Unavailable
private var swapTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<SwapTransactionsState>>()
private var onrampTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<OnrampTransaction>>()
private var expressTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<ExpressTransactionStateUM>>()
private val stateFactory = TokenDetailsStateFactory(
currentStateProvider = Provider { uiState.value },
@ -182,37 +157,15 @@ internal class TokenDetailsViewModel @Inject constructor(
decimals = cryptoCurrency.decimals,
)
private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
ExchangeStatusFactory(
swapTransactionRepository = swapTransactionRepository,
swapRepository = swapRepository,
quotesRepository = quotesRepository,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
addCryptoCurrenciesUseCase = addCryptoCurrenciesUseCase,
swapTransactionStatusStore = swapTransactionStatusStore,
dispatchers = dispatchers,
private val expressStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
expressStatusFactory.create(
clickIntents = this,
appCurrencyProvider = Provider { selectedAppCurrencyFlow.value },
analyticsEventsHandlerProvider = Provider { analyticsEventsHandler },
currentStateProvider = Provider { uiState.value },
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
)
}
private val onrampStatusFactory by lazy(LazyThreadSafetyMode.NONE) {
OnrampStatusFactory(
stateProvider = Provider { uiState.value },
appCurrencyProvider = Provider { selectedAppCurrencyFlow.value },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
clickIntents = this,
getOnrampTransactionsUseCase = getOnrampTransactionsUseCase,
onrampSaveTransactionUseCase = onrampSaveTransactionUseCase,
onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase,
getOnrampStatusUseCase = getOnrampStatusUseCase,
dispatchers = dispatchers,
)
}
@ -255,16 +208,14 @@ internal class TokenDetailsViewModel @Inject constructor(
}
override fun onCleared() {
swapTxStatusTaskScheduler.cancelTask()
onrampTxStatusTaskScheduler.cancelTask()
onrampTxJobHolder.cancel()
expressTxStatusTaskScheduler.cancelTask()
expressTxJobHolder.cancel()
super.onCleared()
}
private fun updateContent() {
subscribeOnCurrencyStatusUpdates()
subscribeOnExchangeTransactionsUpdates()
subscribeOnOnrampTransactionsUpdates()
subscribeOnExpressTransactionsUpdates()
updateTxHistory(refresh = false, showItemsLoading = true)
updateStakingInfo()
@ -337,55 +288,31 @@ internal class TokenDetailsViewModel @Inject constructor(
}
}
private fun subscribeOnExchangeTransactionsUpdates() {
private fun subscribeOnExpressTransactionsUpdates() {
viewModelScope.launch(dispatchers.main) {
swapTxStatusTaskScheduler.cancelTask()
exchangeStatusFactory.invoke()
expressTxStatusTaskScheduler.cancelTask()
expressStatusFactory
.getExpressStatuses()
.distinctUntilChanged()
.filterNot { it.isEmpty() }
.onEach { swapTxs ->
updateSwapTx(swapTxs)
swapTxStatusTaskScheduler.scheduleTask(
viewModelScope,
PeriodicTask(
delay = EXCHANGE_STATUS_UPDATE_DELAY,
task = {
runCatching {
exchangeStatusFactory.updateSwapTxStatuses(internalUiState.value.swapTxs)
}
},
onSuccess = ::updateSwapTx,
onError = { /* no-op */ },
),
.onEach { expressTxs ->
internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs(
expressTxs,
::updateNetworkToSwapBalance,
)
}
.flowOn(dispatchers.main)
.launchIn(viewModelScope)
.saveIn(swapTxJobHolder)
}
}
private fun subscribeOnOnrampTransactionsUpdates() {
viewModelScope.launch(dispatchers.main) {
onrampTxStatusTaskScheduler.cancelTask()
onrampStatusFactory.invoke().distinctUntilChanged()
.filterNot { it.isEmpty() }
.onEach { onrampTxs ->
internalUiState.value = onrampStatusFactory.updateOnrampStatusBottomSheet(onrampTxs)
onrampTxStatusTaskScheduler.scheduleTask(
expressTxStatusTaskScheduler.scheduleTask(
viewModelScope,
PeriodicTask(
delay = EXCHANGE_STATUS_UPDATE_DELAY,
delay = EXPRESS_STATUS_UPDATE_DELAY,
task = {
runCatching {
val onrampTxsToUpdate = onrampTxs.filter { onrampTx ->
internalUiState.value.onrampTxs.any { it.info.txId == onrampTx.txId }
}
onrampStatusFactory.updateOnrmapTxStatuses(onrampTxsToUpdate)
expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.expressTxs)
}
},
onSuccess = {
internalUiState.value = onrampStatusFactory.updateOnrampStatusBottomSheet(onrampTxs)
onSuccess = { updatedTxs ->
internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs(
updatedTxs,
::updateNetworkToSwapBalance,
)
},
onError = { /* no-op */ },
),
@ -393,25 +320,10 @@ internal class TokenDetailsViewModel @Inject constructor(
}
.flowOn(dispatchers.main)
.launchIn(viewModelScope)
.saveIn(onrampTxJobHolder)
.saveIn(expressTxJobHolder)
}
}
private fun updateSwapTx(swapTxs: PersistentList<SwapTransactionsState>) {
val config = internalUiState.value.bottomSheetConfig
val exchangeBottomSheet = config?.content as? ExchangeStatusBottomSheetConfig
val currentTx = swapTxs.firstOrNull { it.txId == exchangeBottomSheet?.value?.txId }
if (currentTx?.activeStatus == ExchangeStatus.Finished) {
updateNetworkToSwapBalance(currentTx.toCryptoCurrency)
}
internalUiState.value = internalUiState.value.copy(
swapTxs = swapTxs,
bottomSheetConfig = currentTx?.let(
stateFactory::updateStateWithExchangeStatusBottomSheet,
) ?: config,
)
}
private fun updateNetworkToSwapBalance(toCryptoCurrency: CryptoCurrency) {
viewModelScope.launch {
updateDelayedCurrencyStatusUseCase(
@ -835,8 +747,7 @@ internal class TokenDetailsViewModel @Inject constructor(
refresh = true,
showItemsLoading = internalUiState.value.txHistoryState !is TxHistoryState.Content,
)
subscribeOnExchangeTransactionsUpdates()
subscribeOnOnrampTransactionsUpdates()
subscribeOnExpressTransactionsUpdates()
},
).awaitAll()
internalUiState.value = stateFactory.getRefreshedState()
@ -844,16 +755,10 @@ internal class TokenDetailsViewModel @Inject constructor(
}
override fun onDismissBottomSheet() {
val bsContent = internalUiState.value.bottomSheetConfig?.content
when (bsContent) {
is ExchangeStatusBottomSheetConfig -> {
when (val bsContent = internalUiState.value.bottomSheetConfig?.content) {
is ExpressStatusBottomSheetConfig -> {
viewModelScope.launch(dispatchers.main) {
internalUiState.value = exchangeStatusFactory.removeTransactionOnBottomSheetClosed()
}
}
is OnrampStatusBottomSheetConfig -> {
viewModelScope.launch(dispatchers.main) {
internalUiState.value = onrampStatusFactory.removeTransactionOnBottomSheetClosed()
expressStatusFactory.removeTransactionOnBottomSheetClosed(bsContent.value)
}
}
}
@ -864,15 +769,9 @@ internal class TokenDetailsViewModel @Inject constructor(
internalUiState.value = stateFactory.getStateWithRemovedRentNotification()
}
override fun onSwapTransactionClick(txId: String) {
val swapTxState = internalUiState.value.swapTxs.first { it.txId == txId }
analyticsEventsHandler.send(TokenExchangeAnalyticsEvent.CexTxStatusOpened(cryptoCurrency.symbol))
internalUiState.value = stateFactory.getStateWithExchangeStatusBottomSheet(swapTxState)
}
override fun onOnrampTransactionClick(txId: String) {
val onrampTxState = internalUiState.value.onrampTxs.first { it.info.txId == txId }
internalUiState.value = onrampStatusFactory.getStateWithOnrampStatusBottomSheet(onrampTxState)
override fun onExpressTransactionClick(txId: String) {
val expressTxState = internalUiState.value.expressTxs.first { it.info.txId == txId }
internalUiState.value = expressStatusFactory.getStateWithExpressStatusBottomSheet(expressTxState)
}
override fun onGoToProviderClick(url: String) {
@ -880,9 +779,13 @@ internal class TokenDetailsViewModel @Inject constructor(
}
override fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency) {
if (internalUiState.value.bottomSheetConfig?.content is ExchangeStatusBottomSheetConfig) {
val bottomSheetState = internalUiState.value.bottomSheetConfig?.content
if (bottomSheetState is ExpressStatusBottomSheetConfig) {
viewModelScope.launch {
internalUiState.value = exchangeStatusFactory.removeTransactionOnBottomSheetClosed(true)
expressStatusFactory.removeTransactionOnBottomSheetClosed(
expressState = bottomSheetState.value,
isForceTerminal = true,
)
}
}
internalUiState.value = stateFactory.getStateWithClosedBottomSheet()
@ -1005,6 +908,6 @@ internal class TokenDetailsViewModel @Inject constructor(
}
private companion object {
const val EXCHANGE_STATUS_UPDATE_DELAY = 10_000L
const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L
}
}