Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-19 19:48:27 +05:00
commit 796ceab5dc
31 changed files with 772 additions and 97 deletions

View file

@ -19,6 +19,7 @@ dependencies {
implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt)
implementation(deps.compose.coil)
implementation(deps.compose.constraintLayout)
/** Deps */
implementation(deps.kotlin.immutable.collections)
@ -34,6 +35,7 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.transaction.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.onramp.models)
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain) {

View file

@ -1,4 +1,4 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
package com.tangem.common.ui.expressStatus
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -8,16 +8,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import com.tangem.common.ui.R
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.inputrow.InputRowApprox
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.tokendetails.impl.R
@Suppress("LongParameterList")
@Composable
internal fun ExpressEstimate(
fun ExpressEstimate(
timestamp: TextReference,
fromTokenIconState: CurrencyIconState,
toTokenIconState: CurrencyIconState,

View file

@ -1,4 +1,4 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
package com.tangem.common.ui.expressStatus
import android.content.res.Configuration
import android.widget.Toast
@ -29,7 +29,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
internal fun ExpressProvider(
fun ExpressProvider(
providerName: TextReference,
providerType: TextReference,
providerTxId: String?,

View file

@ -0,0 +1,24 @@
package com.tangem.common.ui.expressStatus
import androidx.compose.runtime.Composable
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
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
data class ExpressStatusBottomSheetConfig(
val value: ExpressTransactionStateUM,
) : TangemBottomSheetConfigContent
@Composable
fun ExpressStatusBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
containerColor = TangemTheme.colors.background.tertiary,
) { content: ExpressStatusBottomSheetConfig ->
when (val state = content.value) {
is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(state)
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
package com.tangem.common.ui.expressStatus
import android.content.res.Configuration
import androidx.annotation.DrawableRes
@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.constraintlayout.compose.*
import com.tangem.common.ui.R
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
@ -27,7 +28,6 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tokendetails.impl.R
@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod", "LongParameterList")
@Composable

View file

@ -1,14 +1,14 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
package com.tangem.common.ui.expressStatus
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import com.tangem.common.ui.R
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateIconUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.PersistentList
internal fun LazyListScope.expressTransactionsItems(
fun LazyListScope.expressTransactionsItems(
expressTxs: PersistentList<ExpressTransactionStateUM>,
modifier: Modifier = Modifier,
) {

View file

@ -1,4 +1,4 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp
package com.tangem.common.ui.expressStatus
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@ -8,8 +8,7 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import com.tangem.common.ui.expressStatus.ExpressStatusBlock
import com.tangem.common.ui.expressStatus.ExpressStatusNotificationBlock
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12
@ -17,12 +16,9 @@ import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.extensions.stringReference
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.ExpressEstimate
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressProvider
@Composable
internal fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) {
fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) {
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) {
SpacerH10()
Text(

View file

@ -1,31 +1,13 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.express
package com.tangem.common.ui.expressStatus.state
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
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.components.ExchangeStatusNotifications
import kotlinx.collections.immutable.ImmutableList
internal sealed class ExpressTransactionStateUM {
interface ExpressTransactionStateUM {
abstract val info: ExpressTransactionStateInfoUM
data class ExchangeUM(
override val info: ExpressTransactionStateInfoUM,
val provider: SwapProvider,
val activeStatus: ExchangeStatus?,
val statuses: ImmutableList<ExchangeStatusState>,
val notification: ExchangeStatusNotifications? = null,
val showProviderLink: Boolean,
val isRefundTerminalStatus: Boolean,
val fromCryptoCurrency: CryptoCurrency,
val toCryptoCurrency: CryptoCurrency,
) : ExpressTransactionStateUM()
val info: ExpressTransactionStateInfoUM
data class OnrampUM(
override val info: ExpressTransactionStateInfoUM,
@ -34,10 +16,10 @@ internal sealed class ExpressTransactionStateUM {
val providerType: String, // todo onramp fix after SwapProvider moved to own module
val activeStatus: OnrampStatus.Status,
val fromCurrencyCode: String,
) : ExpressTransactionStateUM()
) : ExpressTransactionStateUM
}
internal data class ExpressTransactionStateInfoUM(
data class ExpressTransactionStateInfoUM(
val title: TextReference,
val status: ExpressStatusUM,
val notification: NotificationUM?,
@ -49,7 +31,6 @@ internal data class ExpressTransactionStateInfoUM(
val onGoToProviderClick: (String) -> Unit,
val onClick: () -> Unit,
val iconState: ExpressTransactionStateIconUM,
val toAmount: TextReference,
val toFiatAmount: TextReference?,
val toAmountSymbol: String,
@ -61,7 +42,7 @@ internal data class ExpressTransactionStateInfoUM(
val fromCurrencyIcon: CurrencyIconState,
)
internal enum class ExpressTransactionStateIconUM {
enum class ExpressTransactionStateIconUM {
Warning,
Error,
None,

View file

@ -9,7 +9,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList

View file

@ -0,0 +1,21 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.express
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
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 ExchangeUM(
override val info: ExpressTransactionStateInfoUM,
val provider: SwapProvider,
val activeStatus: ExchangeStatus?,
val statuses: ImmutableList<ExchangeStatusState>,
val notification: ExchangeStatusNotifications? = null,
val showProviderLink: Boolean,
val isRefundTerminalStatus: Boolean,
val fromCryptoCurrency: CryptoCurrency,
val toCryptoCurrency: CryptoCurrency,
) : ExpressTransactionStateUM

View file

@ -23,9 +23,9 @@ 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.tokens.model.analytics.TokenOnrampAnalyticsEvent
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.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.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider

View file

@ -23,9 +23,9 @@ import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListMode
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.common.ui.expressStatus.state.ExpressTransactionStateIconUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
@ -45,20 +45,20 @@ internal class TokenDetailsSwapTransactionsStateConverter(
private val cryptoCurrency: CryptoCurrency,
private val analyticsEventsHandler: AnalyticsEventHandler,
appCurrencyProvider: Provider<AppCurrency>,
) : Converter<Unit, PersistentList<ExpressTransactionStateUM.ExchangeUM>> {
) : Converter<Unit, PersistentList<ExchangeUM>> {
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
private val appCurrency = appCurrencyProvider()
override fun convert(value: Unit): PersistentList<ExpressTransactionStateUM.ExchangeUM> {
override fun convert(value: Unit): PersistentList<ExchangeUM> {
return persistentListOf()
}
fun convert(
savedTransactions: List<SavedSwapTransactionListModel>,
quotes: Set<Quote>,
): PersistentList<ExpressTransactionStateUM.ExchangeUM> {
val result = mutableListOf<ExpressTransactionStateUM.ExchangeUM>()
): PersistentList<ExchangeUM> {
val result = mutableListOf<ExchangeUM>()
savedTransactions
.forEach { swapTransaction ->
@ -84,7 +84,7 @@ internal class TokenDetailsSwapTransactionsStateConverter(
getNotification(transaction.status?.status, transaction.status?.txExternalUrl, null)
val showProviderLink = getShowProviderLink(notifications, transaction.status)
result.add(
ExpressTransactionStateUM.ExchangeUM(
ExchangeUM(
provider = transaction.provider,
statuses = getStatuses(transaction.status?.status),
notification = notifications,
@ -108,11 +108,11 @@ internal class TokenDetailsSwapTransactionsStateConverter(
}
fun updateTxStatus(
tx: ExpressTransactionStateUM.ExchangeUM,
tx: ExchangeUM,
statusModel: ExchangeStatusModel?,
refundToken: CryptoCurrency?,
isRefundTerminalStatus: Boolean,
): ExpressTransactionStateUM.ExchangeUM {
): ExchangeUM {
if (statusModel == null || tx.activeStatus == statusModel.status) {
Timber.e("UpdateTxStatus isn't required. Current status isn't changed")
return tx

View file

@ -1,5 +1,6 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus
import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore
@ -15,9 +16,8 @@ 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.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter
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 dagger.assisted.Assisted
@ -55,7 +55,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
)
}
suspend operator fun invoke(): Flow<PersistentList<ExpressTransactionStateUM.ExchangeUM>> {
suspend operator fun invoke(): Flow<PersistentList<ExchangeUM>> {
val selectedWallet = getSelectedWalletSyncUseCase().fold(
ifLeft = { return emptyFlow() },
ifRight = { it },
@ -82,7 +82,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
suspend fun removeTransactionOnBottomSheetClosed(isForceTerminal: Boolean = false) {
val state = currentStateProvider()
val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return
val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.ExchangeUM ?: return
val selectedTx = bottomSheetConfig.value as? ExchangeUM ?: return
val shouldTerminate = selectedTx.activeStatus.isTerminal(selectedTx.isRefundTerminalStatus) || isForceTerminal
if (shouldTerminate) {
@ -95,7 +95,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
}
}
suspend fun updateSwapTxStatus(swapTx: ExpressTransactionStateUM.ExchangeUM): ExpressTransactionStateUM.ExchangeUM {
suspend fun updateSwapTxStatus(swapTx: ExchangeUM): ExchangeUM {
return if (swapTx.activeStatus.isTerminal(swapTx.isRefundTerminalStatus)) {
swapTx
} else {
@ -163,7 +163,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
private fun getExchangeStatusState(
savedTransactions: List<SavedSwapTransactionListModel>?,
quotes: Set<Quote>,
): PersistentList<ExpressTransactionStateUM.ExchangeUM> {
): PersistentList<ExchangeUM> {
if (savedTransactions == null) {
return persistentListOf()
}

View file

@ -1,5 +1,7 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express
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.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.appcurrency.model.AppCurrency
@ -10,8 +12,7 @@ import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
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.state.express.ExchangeUM
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -79,11 +80,13 @@ internal class ExpressStatusFactory @AssistedInject constructor(
expressTxs.map { tx ->
async {
when (tx) {
is ExpressTransactionStateUM.ExchangeUM -> exchangeStatusFactory.updateSwapTxStatus(tx)
is ExchangeUM -> exchangeStatusFactory.updateSwapTxStatus(tx)
is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.updateOnrmapTxStatus(tx)
else -> null
}
}
}.awaitAll()
.filterNotNull()
.toPersistentList()
}
@ -95,13 +98,13 @@ internal class ExpressStatusFactory @AssistedInject constructor(
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) {
if (currentTx is ExchangeUM && currentTx.activeStatus == ExchangeStatus.Finished) {
updateBalance(currentTx.toCryptoCurrency)
}
val expressTxsToDisplay = expressTxs.filterNot {
when (it) {
is ExpressTransactionStateUM.ExchangeUM -> false
is ExpressTransactionStateUM.OnrampUM -> it.activeStatus.isHidden
else -> false
}
}.toPersistentList()
return state.copy(
@ -115,7 +118,7 @@ internal class ExpressStatusFactory @AssistedInject constructor(
fun getStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): TokenDetailsState {
val analyticEvent = when (expressState) {
is ExpressTransactionStateUM.ExchangeUM -> TokenExchangeAnalyticsEvent.CexTxStatusOpened(
is ExchangeUM -> TokenExchangeAnalyticsEvent.CexTxStatusOpened(
cryptoCurrency.symbol,
)
is ExpressTransactionStateUM.OnrampUM -> TokenOnrampAnalyticsEvent.OnrampStatusOpened(
@ -123,6 +126,7 @@ internal class ExpressStatusFactory @AssistedInject constructor(
provider = expressState.providerName,
fiatCurrency = expressState.fromCurrencyCode,
)
else -> return currentStateProvider()
}
analyticsEventsHandler.send(analyticEvent)
@ -156,9 +160,7 @@ internal class ExpressStatusFactory @AssistedInject constructor(
isForceTerminal: Boolean = false,
) {
when (expressState) {
is ExpressTransactionStateUM.ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(
isForceTerminal,
)
is ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(isForceTerminal)
is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.removeTransactionOnBottomSheetClosed()
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express
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.appcurrency.model.AppCurrency
@ -14,9 +16,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
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.ExpressStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.Provider
import dagger.assisted.Assisted

View file

@ -22,6 +22,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.paging.compose.collectAsLazyPagingItems
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.expressTransactionsItems
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
@ -49,8 +51,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
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.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

View file

@ -1,17 +1,14 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
import androidx.compose.runtime.Composable
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.OnrampStatusBottomSheetContent
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
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.state.express.ExchangeUM
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) {
@ -21,7 +18,7 @@ internal fun ExpressStatusBottomSheet(config: TangemBottomSheetConfig) {
) { content: ExpressStatusBottomSheetConfig ->
when (val state = content.value) {
is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(state)
is ExpressTransactionStateUM.ExchangeUM -> ExchangeStatusBottomSheetContent(state)
is ExchangeUM -> ExchangeStatusBottomSheetContent(state)
}
}
}

View file

@ -9,6 +9,8 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import com.tangem.common.ui.expressStatus.ExpressEstimate
import com.tangem.common.ui.expressStatus.ExpressProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12
@ -19,12 +21,10 @@ 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.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
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
@Composable
internal fun ExchangeStatusBottomSheetContent(state: ExpressTransactionStateUM.ExchangeUM) {
internal fun ExchangeStatusBottomSheetContent(state: ExchangeUM) {
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) {
SpacerH10()
Text(

View file

@ -5,7 +5,6 @@ import androidx.lifecycle.*
import androidx.paging.cachedIn
import arrow.core.getOrElse
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.bundle.unbundle
@ -68,10 +67,10 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.Token
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender
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.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
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.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider

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,
),
)
}
}
}