From 529527cad0071b64840e78b3b77354848979ef20 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Dec 2024 17:04:38 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../factory/express/ExpressStatusFactory.kt | 2 +- features/wallet/impl/build.gradle.kts | 7 +- .../wallet/domain/OnrampStatusFactory.kt | 99 +++++++ .../implementors/SingleWalletContentLoader.kt | 14 + .../SingleWalletContentLoaderFactory.kt | 6 + .../wallet/state/model/WalletState.kt | 3 + .../SetExpressStatusesTransformer.kt | 76 +++++ .../SingleWalletOnrampTransactionConverter.kt | 279 ++++++++++++++++++ .../state/utils/WalletLoadingStateFactory.kt | 2 + .../SingleWalletExpressStatusesSubscriber.kt | 92 ++++++ .../presentation/wallet/ui/WalletScreen.kt | 10 + .../wallet/viewmodels/WalletViewModel.kt | 28 +- .../intents/WalletContentClickIntents.kt | 47 +++ 13 files changed, 659 insertions(+), 6 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt index 0ed77a77b5..7cfbd6eb7b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt @@ -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() diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 88d3ff2a2c..2aa0052b0a 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -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) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt new file mode 100644 index 0000000000..8387de10c1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt index b4c0f3252e..0d96fb7855 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt @@ -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, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt index c6be994278..aa40c79e7f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt @@ -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, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 6a23ccc059..4f86e6685c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -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, override val marketPriceBlockState: MarketPriceBlockState, override val txHistoryState: TxHistoryState, + val expressTxsToDisplay: PersistentList, + val expressTxs: PersistentList, ) : SingleCurrency() data class Locked( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt new file mode 100644 index 0000000000..98058d6c2e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt @@ -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, + 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 + }, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt new file mode 100644 index 0000000000..f6443b6ed5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt @@ -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 { + + 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 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index aa619fe1e2..45b1b98237 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -58,6 +58,8 @@ internal class WalletLoadingStateFactory( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), ), ), + expressTxsToDisplay = persistentListOf(), + expressTxs = persistentListOf(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt new file mode 100644 index 0000000000..37eb922c67 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt @@ -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, 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.clearHiddenTerminal() { + this.filter { it.status.isHidden && it.status.isTerminal } + .forEach { onrampRemoveTransactionUseCase(externalTxId = it.externalTxId) } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 2617c1d2e8..52a0fea363 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -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) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index a9af33b972..785d29d72e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -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() + 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 } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index 32132d1dfc..0b20b60c75 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -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, + ), + ) + } + } } \ No newline at end of file