Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-19 17:04:38 +05:00
parent 47f9a36fb4
commit 529527cad0
13 changed files with 659 additions and 6 deletions

View file

@ -103,7 +103,7 @@ internal class ExpressStatusFactory @AssistedInject constructor(
}
val expressTxsToDisplay = expressTxs.filterNot {
when (it) {
is ExpressTransactionStateUM.OnrampUM -> false // it.activeStatus.isHidden
is ExpressTransactionStateUM.OnrampUM -> it.activeStatus.isHidden
else -> false
}
}.toPersistentList()

View file

@ -52,18 +52,19 @@ dependencies {
implementation(projects.core.utils)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.common.routing)
implementation(projects.core.deepLinks)
implementation(projects.core.deepLinks.global)
implementation(projects.core.decompose)
implementation(projects.core.datasource)
implementation(projects.libs.crypto)
implementation(projects.libs.blockchainSdk)
/** Domain modules */
implementation(projects.domain.card)
implementation(projects.domain.demo)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.models)
implementation(projects.domain.settings)
implementation(projects.domain.tokens)
@ -82,6 +83,7 @@ dependencies {
implementation(projects.domain.markets.models)
implementation(projects.domain.feedback)
implementation(projects.domain.onramp.models)
implementation(projects.domain.onramp)
//TODO: Create api/impl modules for onboarding [REDACTED_JIRA]
implementation(projects.features.onboarding)
@ -100,6 +102,7 @@ dependencies {
/** Common modules */
implementation(projects.common.ui)
implementation(projects.common.routing)
/** Test libraries */
implementation(deps.test.junit)

View file

@ -0,0 +1,99 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus
import com.tangem.domain.onramp.GetOnrampStatusUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.onramp.OnrampUpdateTransactionStatusUseCase
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.OnrampStatus.Status.*
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@ViewModelScoped
internal class OnrampStatusFactory @Inject constructor(
private val stateHolder: WalletStateController,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
private val getOnrampStatusUseCase: GetOnrampStatusUseCase,
private val onrampUpdateTransactionStatusUseCase: OnrampUpdateTransactionStatusUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend fun removeTransactionOnBottomSheetClosed() {
val state = stateHolder.getSelectedWallet()
val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return
val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.OnrampUM ?: return
if (selectedTx.activeStatus.isTerminal) {
onrampRemoveTransactionUseCase(externalTxId = selectedTx.info.txExternalId)
}
}
suspend fun updateOnrmapTransactionStatuses() = withContext(dispatchers.io) {
val singleWalletState = stateHolder.getSelectedWallet() as? WalletState.SingleCurrency.Content
?: return@withContext
singleWalletState.expressTxs.map { tx ->
async {
if (tx is ExpressTransactionStateUM.OnrampUM) {
updateOnrampTxStatus(tx)
}
}
}.awaitAll()
}
private suspend fun updateOnrampTxStatus(onrampTx: ExpressTransactionStateUM.OnrampUM) {
if (!onrampTx.activeStatus.isTerminal) {
getOnrampStatusUseCase(onrampTx.info.txId).fold(
ifLeft = {
Timber.e("Couldn't update onramp status. $it")
},
ifRight = { statusModel ->
val externalTxId = statusModel.externalTxId
val status = toAnalyticStatus(statusModel.status) ?: return
if (statusModel.status != onrampTx.activeStatus) {
analyticsEventHandler.send(
TokenOnrampAnalyticsEvent.OnrampStatusChanged(
tokenSymbol = onrampTx.info.toAmountSymbol,
status = status.name,
provider = onrampTx.providerName,
fiatCurrency = onrampTx.fromCurrencyCode,
),
)
onrampUpdateTransactionStatusUseCase(externalTxId = externalTxId, statusModel.status)
}
},
)
}
}
private fun toAnalyticStatus(status: OnrampStatus.Status?): ExpressAnalyticsStatus? {
return when (status) {
Expired,
Paused,
-> ExpressAnalyticsStatus.Cancelled
Created,
WaitingForPayment,
PaymentProcessing,
Paid,
Sending,
-> ExpressAnalyticsStatus.InProgress
Verifying -> ExpressAnalyticsStatus.KYC
Failed -> ExpressAnalyticsStatus.Fail
Finished -> ExpressAnalyticsStatus.Done
null -> null
}
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.onramp.GetOnrampTransactionsUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
@ -27,6 +29,8 @@ internal class SingleWalletContentLoader(
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
) : WalletContentLoader(id = userWallet.walletId) {
@ -55,6 +59,16 @@ internal class SingleWalletContentLoader(
getSingleWalletWarningsFactory = getSingleWalletWarningsFactory,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
),
SingleWalletExpressStatusesSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
clickIntents = clickIntents,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
analyticsEventHandler = analyticsEventHandler,
getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase,
getOnrampTransactionsUseCase = getOnrampTransactionsUseCase,
onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase,
),
TxHistorySubscriber(
userWallet = userWallet,
isRefresh = isRefresh,

View file

@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.onramp.GetOnrampTransactionsUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
@ -26,6 +28,8 @@ internal class SingleWalletContentLoaderFactory @Inject constructor(
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
) {
@ -45,6 +49,8 @@ internal class SingleWalletContentLoaderFactory @Inject constructor(
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
analyticsEventHandler = analyticsEventHandler,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
getOnrampTransactionsUseCase = getOnrampTransactionsUseCase,
onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase,
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.state.model
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
@ -59,6 +60,8 @@ internal sealed interface WalletState : WalletStateHolder {
override val buttons: PersistentList<WalletManageButton>,
override val marketPriceBlockState: MarketPriceBlockState,
override val txHistoryState: TxHistoryState,
val expressTxsToDisplay: PersistentList<ExpressTransactionStateUM>,
val expressTxs: PersistentList<ExpressTransactionStateUM>,
) : SingleCurrency()
data class Locked(

View file

@ -0,0 +1,76 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
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.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletOnrampTransactionConverter
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import kotlinx.collections.immutable.toPersistentList
import timber.log.Timber
internal class SetExpressStatusesTransformer(
userWalletId: UserWalletId,
private val onrampTxs: List<OnrampTransaction>,
private val clickIntents: WalletClickIntents,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,
private val analyticsEventHandler: AnalyticsEventHandler,
) : WalletStateTransformer(userWalletId) {
override fun transform(prevState: WalletState): WalletState {
return when (prevState) {
is WalletState.SingleCurrency.Content -> {
val expressTxs = SingleWalletOnrampTransactionConverter(
clickIntents = clickIntents,
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrency = appCurrency,
analyticsEventHandler = analyticsEventHandler,
).convertList(onrampTxs).toPersistentList()
val expressTxsToDisplay = expressTxs.filterNot {
it.activeStatus.isHidden
}.toPersistentList()
val expressBottomSheet = prevState.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig
val currentTx = expressTxs.firstOrNull { it.info.txId == expressBottomSheet?.value?.info?.txId }
prevState.copy(
expressTxs = expressTxs,
expressTxsToDisplay = expressTxsToDisplay,
bottomSheetConfig = prevState.bottomSheetConfig?.updateStateWithExpressStatusBottomSheet(currentTx),
)
}
is WalletState.SingleCurrency.Locked -> {
Timber.w("Impossible to load express statuses for locked wallet")
prevState
}
is WalletState.Visa -> {
Timber.w("Impossible to load express statuses for visa wallet")
prevState
}
is WalletState.MultiCurrency -> {
Timber.w("Impossible to load express statuses for multi-currency wallet")
prevState
}
}
}
private fun TangemBottomSheetConfig.updateStateWithExpressStatusBottomSheet(
expressState: ExpressTransactionStateUM?,
): TangemBottomSheetConfig {
val currentConfig = this.content as? ExpressStatusBottomSheetConfig ?: return this
if (expressState == null) return this
return copy(
content = if (currentConfig.value != expressState) {
ExpressStatusBottomSheetConfig(expressState)
} else {
currentConfig
},
)
}
}

View file

@ -0,0 +1,279 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.common.ui.notifications.ExpressNotificationsUM
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.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.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.feature.wallet.impl.R
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
internal class SingleWalletOnrampTransactionConverter(
private val clickIntents: WalletClickIntents,
cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Converter<OnrampTransaction, ExpressTransactionStateUM.OnrampUM> {
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
private val currency = cryptoCurrencyStatus.currency
private val status = cryptoCurrencyStatus.value
override fun convert(value: OnrampTransaction): ExpressTransactionStateUM.OnrampUM {
return ExpressTransactionStateUM.OnrampUM(
info = ExpressTransactionStateInfoUM(
title = resourceReference(
id = R.string.express_status_buying,
wrappedList(currency.name),
),
status = convertStatuses(value.status, value.externalTxUrl),
notification = getNotification(value.status, value.externalTxUrl, value.providerName),
txId = value.txId,
txExternalId = value.externalTxId,
txExternalUrl = value.externalTxUrl,
timestamp = value.timestamp,
timestampFormatted = resourceReference(
R.string.send_date_format,
wrappedList(
value.timestamp.toDateFormatWithTodayYesterday(),
value.timestamp.toTimeFormat(),
),
),
toAmount = stringReference(
value.toAmount.format { crypto(currency) },
),
toFiatAmount = stringReference(
status.fiatRate?.multiply(value.toAmount).format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
),
toAmountSymbol = currency.symbol,
toCurrencyIcon = iconStateConverter.convert(currency),
fromAmount = stringReference(
value.fromAmount.format {
fiat(
fiatCurrencyCode = value.fromCurrency.name,
fiatCurrencySymbol = value.fromCurrency.code,
)
},
),
fromFiatAmount = null,
fromAmountSymbol = value.fromCurrency.code,
fromCurrencyIcon = CurrencyIconState.FiatIcon(
url = value.fromCurrency.image,
fallbackResId = R.drawable.ic_currency_24,
),
iconState = getIconState(value.status),
onGoToProviderClick = {
analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider)
clickIntents.onGoToProviderClick(it)
},
onClick = {
val analyticEvent = TokenOnrampAnalyticsEvent.OnrampStatusOpened(
tokenSymbol = currency.symbol,
provider = value.providerName,
fiatCurrency = value.fromCurrency.code,
)
analyticsEventHandler.send(analyticEvent)
clickIntents.onExpressTransactionClick(value.txId)
},
),
providerName = value.providerName,
providerImageUrl = value.providerImageUrl,
providerType = value.providerType,
activeStatus = value.status,
fromCurrencyCode = value.fromCurrency.code,
)
}
private fun getNotification(
status: OnrampStatus.Status,
externalTxUrl: String?,
providerName: String,
): NotificationUM? {
if (externalTxUrl == null) return null
return when (status) {
OnrampStatus.Status.Verifying -> {
analyticsEventHandler.send(
TokenOnrampAnalyticsEvent.NoticeKYC(currency.symbol, providerName),
)
ExpressNotificationsUM.NeedVerification {
clickIntents.onGoToProviderClick(externalTxUrl)
}
}
OnrampStatus.Status.Failed -> {
ExpressNotificationsUM.FailedByProvider {
clickIntents.onGoToProviderClick(externalTxUrl)
}
}
else -> null
}
}
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(
getAwaitingDepositItem(),
getPaymentProcessingItem(),
getBuyingItem(),
getSendingItem(),
)
}
return ExpressStatusUM(
title = resourceReference(R.string.common_transaction_status),
link = getStatusLink(status, externalTxUrl),
statuses = statuses,
)
}
private fun OnrampStatus.Status.getAwaitingDepositItem() = ExpressStatusItemUM(
text = when {
order < OnrampStatus.Status.WaitingForPayment.order -> {
resourceReference(R.string.express_exchange_status_receiving)
}
this == OnrampStatus.Status.WaitingForPayment -> {
resourceReference(R.string.express_exchange_status_receiving_active)
}
else -> {
resourceReference(R.string.express_exchange_status_received)
}
},
state = getStatusState(OnrampStatus.Status.WaitingForPayment),
)
private fun OnrampStatus.Status.getPaymentProcessingItem() = ExpressStatusItemUM(
text = when {
order < OnrampStatus.Status.PaymentProcessing.order -> {
resourceReference(R.string.express_exchange_status_confirming)
}
this == OnrampStatus.Status.PaymentProcessing -> {
resourceReference(R.string.express_exchange_status_confirming_active)
}
this == OnrampStatus.Status.Verifying -> {
resourceReference(R.string.express_exchange_status_verifying)
}
this == OnrampStatus.Status.Failed -> {
resourceReference(R.string.express_exchange_status_failed)
}
else -> resourceReference(R.string.express_exchange_status_confirmed)
},
state = when {
order < OnrampStatus.Status.PaymentProcessing.order -> {
ExpressStatusItemState.Default
}
this == OnrampStatus.Status.PaymentProcessing -> {
ExpressStatusItemState.Active
}
this == OnrampStatus.Status.Verifying -> {
ExpressStatusItemState.Warning
}
this == OnrampStatus.Status.Failed -> {
ExpressStatusItemState.Error
}
else -> {
ExpressStatusItemState.Done
}
},
)
private fun OnrampStatus.Status.getBuyingItem() = ExpressStatusItemUM(
text = when {
order < OnrampStatus.Status.Paid.order -> {
resourceReference(R.string.express_status_buying, wrappedList(currency.name))
}
this == OnrampStatus.Status.Paid -> {
resourceReference(
R.string.express_status_buying_active,
wrappedList(currency.name),
)
}
else -> {
resourceReference(R.string.express_status_bought, wrappedList(currency.name))
}
},
state = getStatusState(OnrampStatus.Status.Paid),
)
private fun OnrampStatus.Status.getSendingItem() = ExpressStatusItemUM(
text = when {
order < OnrampStatus.Status.Sending.order -> {
resourceReference(
R.string.express_exchange_status_sending,
wrappedList(currency.name),
)
}
this == OnrampStatus.Status.Sending -> {
resourceReference(
R.string.express_exchange_status_sending_active,
wrappedList(currency.name),
)
}
else -> {
resourceReference(
R.string.express_exchange_status_sent,
wrappedList(currency.name),
)
}
},
state = getStatusState(OnrampStatus.Status.Sending),
)
private fun getStatusLink(status: OnrampStatus.Status, externalTxUrl: String?): ExpressLinkUM {
if (externalTxUrl == null) return ExpressLinkUM.Empty
return when (status) {
OnrampStatus.Status.Verifying,
OnrampStatus.Status.Failed,
-> {
ExpressLinkUM.Content(
icon = R.drawable.ic_arrow_top_right_24,
text = resourceReference(R.string.common_go_to_provider),
onClick = {
clickIntents.onGoToProviderClick(externalTxUrl)
},
)
}
else -> ExpressLinkUM.Empty
}
}
private fun OnrampStatus.Status.getStatusState(targetState: OnrampStatus.Status) = when {
order < targetState.order -> ExpressStatusItemState.Default
this == targetState -> ExpressStatusItemState.Active
else -> ExpressStatusItemState.Done
}
}

View file

@ -58,6 +58,8 @@ internal class WalletLoadingStateFactory(
value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick),
),
),
expressTxsToDisplay = persistentListOf(),
expressTxs = persistentListOf(),
)
}

View file

@ -0,0 +1,92 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.onramp.GetOnrampTransactionsUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetExpressStatusesTransformer
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import timber.log.Timber
@Suppress("LongParameterList")
internal class SingleWalletExpressStatusesSubscriber(
private val userWallet: UserWallet,
private val stateHolder: WalletStateController,
private val clickIntents: WalletClickIntents,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
) : WalletSubscriber() {
override fun create(
coroutineScope: CoroutineScope,
): Flow<Pair<Either<CurrencyStatusError, CryptoCurrencyStatus>, AppCurrency>> {
return combine(
flow = getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWallet.walletId)
.conflate()
.distinctUntilChanged(),
flow2 = getSelectedAppCurrencyUseCase()
.conflate()
.distinctUntilChanged()
.map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } },
transform = { maybeCurrencyStatus, appCurrency -> maybeCurrencyStatus to appCurrency },
).onEach { maybeCurrencyStatusAndAppCurrency ->
val status = maybeCurrencyStatusAndAppCurrency.first.getOrElse {
Timber.e("Unable to get primary currency status: $it")
return@onEach
}
getOnrampTransactionsUseCase(
userWalletId = userWallet.walletId,
cryptoCurrencyId = status.currency.id,
).onEach { maybeTransaction ->
maybeTransaction.fold(
ifRight = { onrampTxs ->
onrampTxs.clearHiddenTerminal()
stateHolder.update(
SetExpressStatusesTransformer(
userWalletId = userWallet.walletId,
onrampTxs = onrampTxs,
clickIntents = clickIntents,
cryptoCurrencyStatus = status,
appCurrency = maybeCurrencyStatusAndAppCurrency.second,
analyticsEventHandler = analyticsEventHandler,
),
)
},
ifLeft = {
stateHolder.update(
SetExpressStatusesTransformer(
userWalletId = userWallet.walletId,
onrampTxs = listOf(),
clickIntents = clickIntents,
cryptoCurrencyStatus = status,
appCurrency = maybeCurrencyStatusAndAppCurrency.second,
analyticsEventHandler = analyticsEventHandler,
),
)
},
)
}
.launchIn(coroutineScope)
}
}
private suspend fun List<OnrampTransaction>.clearHiddenTerminal() {
this.filter { it.status.isHidden && it.status.isTerminal }
.forEach { onrampRemoveTransactionUseCase(externalTxId = it.externalTxId) }
}
}

View file

@ -45,6 +45,9 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheet
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.expressTransactionsItems
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
@ -216,6 +219,12 @@ private fun WalletContent(
walletState.marketPriceBlockState?.let { marketPriceBlockState ->
marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier)
}
if (walletState is WalletState.SingleCurrency.Content) {
expressTransactionsItems(
expressTxs = walletState.expressTxsToDisplay,
modifier = itemModifier,
)
}
}
(selectedWallet as? WalletState.Visa.Content)?.let {
@ -716,6 +725,7 @@ private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
is BalancesAndLimitsBottomSheetConfig -> BalancesAndLimitsBottomSheet(config = bottomSheetConfig)
is VisaTxDetailsBottomSheetConfig -> VisaTxDetailsBottomSheet(config = bottomSheetConfig)
is PushNotificationsBottomSheetConfig -> PushNotificationsBottomSheet(config = bottomSheetConfig)
is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(config = bottomSheetConfig)
}
}
}

View file

@ -17,6 +17,7 @@ import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
@ -33,9 +34,7 @@ import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.coroutines.*
import com.tangem.utils.extensions.indexOfFirstOrNull
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
@ -70,6 +69,7 @@ internal class WalletViewModel @Inject constructor(
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
private val walletImageResolver: WalletImageResolver,
private val tokenListStore: MultiWalletTokenListStore,
private val onrampStatusFactory: OnrampStatusFactory,
private val walletFeatureToggles: WalletFeatureToggles,
analyticsEventsHandler: AnalyticsEventHandler,
) : ViewModel() {
@ -81,6 +81,8 @@ internal class WalletViewModel @Inject constructor(
private val refreshWalletJobHolder = JobHolder()
private var needToRefreshWallet = false
private var expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>()
init {
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened)
@ -93,6 +95,7 @@ internal class WalletViewModel @Inject constructor(
subscribeOnSelectedWalletFlow()
subscribeToScreenBackgroundState()
subscribeOnPushNotificationsPermission()
subscribeOnExpressTransactionsUpdates()
}
private fun maybeMigrateNames() {
@ -239,6 +242,24 @@ internal class WalletViewModel @Inject constructor(
.launchIn(viewModelScope)
}
private fun subscribeOnExpressTransactionsUpdates() {
viewModelScope.launch(dispatchers.main) {
expressTxStatusTaskScheduler.cancelTask()
expressTxStatusTaskScheduler.scheduleTask(
viewModelScope,
PeriodicTask(
isDelayFirst = false,
delay = EXPRESS_STATUS_UPDATE_DELAY,
task = {
runCatching { onrampStatusFactory.updateOnrmapTransactionStatuses() }
},
onSuccess = { /* no-op */ },
onError = { /* no-op */ },
),
)
}
}
private fun needToRefreshTimer() {
viewModelScope.launch {
delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS)
@ -430,5 +451,6 @@ internal class WalletViewModel @Inject constructor(
private companion object {
const val REFRESH_WALLET_BACKGROUND_TIMER_MILLIS = 10000L
const val EXPRESS_STATUS_UPDATE_DELAY = 10000L
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
import arrow.core.getOrElse
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
@ -11,10 +12,14 @@ import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory
import com.tangem.feature.wallet.presentation.wallet.domain.unwrap
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCurrencyActionsConverter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.scopes.ViewModelScoped
@ -43,6 +48,12 @@ internal interface WalletContentClickIntents {
fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onTransactionClick(txHash: String)
fun onDissmissBottomSheet()
fun onGoToProviderClick(externalTxId: String)
fun onExpressTransactionClick(txId: String)
}
@Suppress("LongParameterList")
@ -51,6 +62,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
private val stateHolder: WalletStateController,
private val currencyActionsClickIntents: WalletCurrencyActionsClickIntentsImplementor,
private val walletWarningsClickIntents: WalletWarningsClickIntentsImplementor,
private val onrampStatusFactory: OnrampStatusFactory,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
@ -164,4 +176,39 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
)
}
}
override fun onGoToProviderClick(externalTxUrl: String) {
router.openUrl(externalTxUrl)
}
override fun onDissmissBottomSheet() {
val userWalletId = stateHolder.getSelectedWalletId()
if (stateHolder.getSelectedWallet().bottomSheetConfig?.content is ExpressStatusBottomSheetConfig) {
viewModelScope.launch(dispatchers.main) {
onrampStatusFactory.removeTransactionOnBottomSheetClosed()
}
}
stateHolder.update(CloseBottomSheetTransformer(userWalletId))
}
override fun onExpressTransactionClick(txId: String) {
viewModelScope.launch {
val userWalletId = stateHolder.getSelectedWalletId()
val singleWalletState = stateHolder.getSelectedWallet() as? WalletState.SingleCurrency.Content
?: return@launch
val expressTransaction = singleWalletState.expressTxsToDisplay.firstOrNull { it.info.txId == txId }
?: return@launch
stateHolder.update(
OpenBottomSheetTransformer(
userWalletId = userWalletId,
content = ExpressStatusBottomSheetConfig(
value = expressTransaction,
),
onDismissBottomSheet = ::onDissmissBottomSheet,
),
)
}
}
}