Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-12 16:48:30 +04:00
parent ed6970db05
commit 207fe02586
15 changed files with 433 additions and 253 deletions

View file

@ -16,6 +16,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
@ -37,9 +38,13 @@ internal class DefaultRampManager(
private val cryptoCurrencyConverter = CryptoCurrencyConverter(excludedBlockchains)
override fun isSellSupportedByService(cryptoCurrency: CryptoCurrency): Boolean {
return exchangeService?.availableForSell(
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
) ?: false
return runCatching {
exchangeService?.availableForSell(
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
)
}
.getOrNull()
?: false
}
override suspend fun availableForBuy(
@ -47,28 +52,38 @@ internal class DefaultRampManager(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Boolean {
return when {
onrampFeatureToggles.isFeatureEnabled -> getOnrampAvailable(userWalletId, cryptoCurrency)
exchangeService != null -> exchangeService.availableForBuy(
scanResponse = scanResponse,
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
)
else -> false
return runCatching {
when {
onrampFeatureToggles.isFeatureEnabled -> getOnrampAvailable(userWalletId, cryptoCurrency)
exchangeService != null -> exchangeService.availableForBuy(
scanResponse = scanResponse,
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
)
else -> false
}
}
.getOrNull()
?: false
}
override suspend fun availableForSell(userWalletId: UserWalletId, status: CryptoCurrencyStatus): Boolean {
val sellSupportedByService = isSellSupportedByService(cryptoCurrency = status.currency)
return runCatching {
val sellSupportedByService = isSellSupportedByService(cryptoCurrency = status.currency)
if (!sellSupportedByService) return false
if (!sellSupportedByService) return false
val reason = getSendUnavailabilityReason(userWalletId, status)
val reason = getSendUnavailabilityReason(userWalletId, status)
return reason == ScenarioUnavailabilityReason.None
reason == ScenarioUnavailabilityReason.None
}
.getOrNull()
?: false
}
override suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom
return runCatching { getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom }
.getOrNull()
?: false
}
override fun getBuyInitializationStatus(): Flow<ExchangeServiceInitializationStatus> {
@ -76,7 +91,7 @@ internal class DefaultRampManager(
}
override suspend fun fetchBuyServiceData() {
withContext(dispatchers.io) {
runCatching(dispatchers.io) {
buyService.invoke().update()
}
}
@ -86,12 +101,12 @@ internal class DefaultRampManager(
}
override suspend fun fetchSellServiceData() {
withContext(dispatchers.io) {
runCatching(dispatchers.io) {
sellService.invoke().update()
}
}
override fun getSwapInitializationStatus(userWalletId: UserWalletId): Flow<ExchangeServiceInitializationStatus> {
override fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow<ExchangeServiceInitializationStatus> {
return expressServiceLoader.getInitializationStatus(userWalletId)
}

View file

@ -240,6 +240,11 @@ sealed class NotificationUM(val config: NotificationConfig) {
onClick = onRefresh,
),
)
data object SwapNoAvailablePair : Warning(
title = resourceReference(id = R.string.action_buttons_swap_no_available_pair_notification_title),
subtitle = resourceReference(id = R.string.action_buttons_swap_no_available_pair_notification_message),
)
}
open class Info(

View file

@ -39,5 +39,5 @@ interface RampStateManager {
fun getSellInitializationStatus(): Flow<Lce<Throwable, Any>>
fun getSwapInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, Any>>
fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, Any>>
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.onramp.swap.availablepairs.entity.transformers
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.feature.swap.domain.models.ExpressException
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import kotlinx.collections.immutable.persistentListOf
/**
[REDACTED_AUTHOR]
*/
internal class SetErrorWarningTransformer(
private val cause: Throwable,
private val onRefresh: () -> Unit,
) : TokenListUMTransformer {
override fun transform(prevState: TokenListUM): TokenListUM {
return prevState.copy(
availableItems = persistentListOf(),
unavailableItems = persistentListOf(),
warning = NotificationUM.Warning.OnrampErrorNotification(
errorCode = (cause as? ExpressException)?.expressDataError?.code?.toString(),
onRefresh = onRefresh,
),
)
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
/**
@ -18,6 +19,8 @@ internal class SetLoadingTokenItemsTransformer(
override fun transform(prevState: TokenListUM): TokenListUM {
return prevState.copy(
availableItems = LoadingTokenListItemConverter.convertList(input = statuses).toImmutableList(),
unavailableItems = persistentListOf(),
warning = null,
)
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.features.onramp.swap.availablepairs.entity.transformers
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
import com.tangem.features.onramp.tokenlist.entity.utils.addHeader
import kotlinx.collections.immutable.persistentListOf
/**
[REDACTED_AUTHOR]
*/
internal class SetNoAvailablePairsTransformer(
private val appCurrency: AppCurrency,
private val unavailableStatuses: List<CryptoCurrencyStatus>,
private val isBalanceHidden: Boolean,
private val unavailableTokensHeaderReference: TextReference,
) : TokenListUMTransformer {
override fun transform(prevState: TokenListUM): TokenListUM {
val unavailableItems = OnrampTokenItemStateConverterFactory.createUnavailableItemConverter(appCurrency)
.convertList(unavailableStatuses)
.map(TokensListItemUM::Token)
return prevState.copy(
availableItems = persistentListOf(),
unavailableItems = unavailableItems.addHeader(textReference = unavailableTokensHeaderReference),
isBalanceHidden = isBalanceHidden,
warning = NotificationUM.Warning.SwapNoAvailablePair,
)
}
}

View file

@ -9,7 +9,11 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -19,9 +23,12 @@ import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
import com.tangem.feature.swap.domain.models.domain.SwapPairLeast
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoadingTokenItemsTransformer
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer
import com.tangem.features.onramp.utils.InputManager
@ -33,11 +40,13 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
private typealias AvailablePairsState = Lce<Throwable, List<SwapPairLeast>>
@Suppress("LongParameterList")
internal class AvailableSwapPairsModel @Inject constructor(
paramsContainer: ParamsContainer,
getTokenListUseCase: GetTokenListUseCase,
override val dispatchers: CoroutineDispatcherProvider,
private val getTokenListUseCase: GetTokenListUseCase,
private val tokenListUMController: TokenListUMController,
private val searchManager: InputManager,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
@ -49,29 +58,37 @@ internal class AvailableSwapPairsModel @Inject constructor(
private var params: AvailableSwapPairsComponent.Params = paramsContainer.require()
private val tokenListFlow = getTokenListUseCase.launch(userWalletId = params.userWalletId)
.distinctUntilChanged()
.map { maybeTokenList ->
maybeTokenList.getOrElse(
ifLoading = { it ?: TokenList.Empty },
ifError = { TokenList.Empty },
)
.flattenCurrencies()
}
.shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1)
private val tokenListFlow = getTokenListUseCaseFlow()
private val availablePairsByNetworkFlow = MutableStateFlow<Map<LeastTokenInfo, List<SwapPairLeast>>>(emptyMap())
private val availablePairsByNetworkFlow = MutableStateFlow<Map<LeastTokenInfo, AvailablePairsState>>(emptyMap())
init {
initializeSearchBardCallbacks()
subscribeOnUpdateState()
subscribeOnAvailablePairsUpdates()
}
private fun getTokenListUseCaseFlow(): SharedFlow<List<CryptoCurrencyStatus>> {
return getTokenListUseCase.launch(userWalletId = params.userWalletId)
.distinctUntilChanged()
.map { maybeTokenList ->
maybeTokenList.getOrElse(
ifLoading = { it ?: TokenList.Empty },
ifError = { TokenList.Empty },
)
.flattenCurrencies()
}
.shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1)
}
private fun initializeSearchBardCallbacks() {
tokenListUMController.update(
transformer = UpdateSearchBarCallbacksTransformer(
onQueryChange = ::onSearchQueryChange,
onActiveChange = ::onSearchBarActiveChange,
),
)
subscribeOnUpdateState()
subscribeOnAvailablePairsUpdates()
}
private fun subscribeOnUpdateState() {
@ -81,69 +98,143 @@ internal class AvailableSwapPairsModel @Inject constructor(
flow3 = params.selectedStatus,
flow4 = searchManager.query,
flow5 = availablePairsByNetworkFlow
.map { it[params.selectedStatus.value?.toLeastTokenInfo()].orEmpty() }
.map { it[params.selectedStatus.value?.toLeastTokenInfo()] }
.distinctUntilChanged(),
) { currencies, appCurrencyAndBalanceHiding, selectedStatus, query, availablePairs ->
if (availablePairs.isEmpty()) {
SetLoadingTokenItemsTransformer(currencies)
} else {
val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding
val filterByQueryTokenList = currencies
.filter { it.currency != selectedStatus?.currency }
.filterByQuery(query = query)
if (query.isNotEmpty() && filterByQueryTokenList.isEmpty()) {
SetNothingToFoundStateTransformer(
isBalanceHidden = isBalanceHidden,
emptySearchMessageReference = resourceReference(
id = R.string.action_buttons_swap_empty_search_message,
),
) { currencies, appCurrencyAndBalanceHiding, selectedStatus, query, availablePairsState ->
availablePairsState?.fold(
ifLoading = { SetLoadingTokenItemsTransformer(currencies) },
ifContent = { pairs ->
handleContentState(
appCurrencyAndBalanceHiding = appCurrencyAndBalanceHiding,
currencies = currencies,
selectedStatus = selectedStatus,
query = query,
availablePairs = pairs,
)
} else {
UpdateTokenItemsTransformer(
appCurrency = appCurrency,
onItemClick = params.onTokenClick,
statuses = filterByQueryTokenList.filterByAvailability(availablePairs = availablePairs),
isBalanceHidden = isBalanceHidden,
unavailableTokensHeaderReference = resourceReference(
id = R.string.tokens_list_unavailable_to_swap_header,
wrappedList(selectedStatus?.currency?.name?.capitalize() ?: ""),
),
},
ifError = {
handleErrorState(
cause = it,
networkInfo = params.selectedStatus.value?.toLeastTokenInfo(),
currencies = currencies,
)
}
}
},
)
?: SetLoadingTokenItemsTransformer(currencies)
}
.onEach(tokenListUMController::update)
.flowOn(dispatchers.main)
.launchIn(modelScope)
}
private fun handleContentState(
appCurrencyAndBalanceHiding: Pair<AppCurrency, Boolean>,
currencies: List<CryptoCurrencyStatus>,
selectedStatus: CryptoCurrencyStatus?,
query: String,
availablePairs: List<SwapPairLeast>,
): TokenListUMTransformer {
val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding
if (availablePairs.isEmpty()) {
return SetNoAvailablePairsTransformer(
appCurrency = appCurrency,
unavailableStatuses = currencies,
isBalanceHidden = isBalanceHidden,
unavailableTokensHeaderReference = resourceReference(
id = R.string.tokens_list_unavailable_to_swap_header,
wrappedList(selectedStatus?.currency?.name?.capitalize() ?: ""),
),
)
}
val filterByQueryTokenList = currencies
.filter { it.currency != selectedStatus?.currency }
.filterByQuery(query = query)
return if (query.isNotEmpty() && filterByQueryTokenList.isEmpty()) {
SetNothingToFoundStateTransformer(
isBalanceHidden = isBalanceHidden,
emptySearchMessageReference = resourceReference(
id = R.string.action_buttons_swap_empty_search_message,
),
)
} else {
UpdateTokenItemsTransformer(
appCurrency = appCurrency,
onItemClick = params.onTokenClick,
statuses = filterByQueryTokenList.filterByAvailability(availablePairs = availablePairs),
isBalanceHidden = isBalanceHidden,
unavailableTokensHeaderReference = resourceReference(
id = R.string.tokens_list_unavailable_to_swap_header,
wrappedList(selectedStatus?.currency?.name?.capitalize() ?: ""),
),
)
}
}
private fun handleErrorState(
cause: Throwable,
networkInfo: LeastTokenInfo?,
currencies: List<CryptoCurrencyStatus>,
): SetErrorWarningTransformer {
return SetErrorWarningTransformer(
cause = cause,
onRefresh = {
modelScope.launch {
if (networkInfo != null) {
updateAvailablePairs(networkInfo, currencies)
}
}
},
)
}
private fun subscribeOnAvailablePairsUpdates() {
modelScope.launch {
params.selectedStatus
.filterNotNull()
.collectLatest { selectedStatus ->
val initialCurrency = selectedStatus.toLeastTokenInfo()
val tokenList = tokenListFlow.firstOrNull() ?: return@collectLatest
val networkInfo = selectedStatus.toLeastTokenInfo()
val availablePairs = availablePairsByNetworkFlow.value[initialCurrency]
if (!availablePairs.isNullOrEmpty()) return@collectLatest
val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() ?: false
if (isAlreadyLoaded) return@collectLatest
val pairs = getAvailablePairsUseCase(
initialCurrency = initialCurrency,
currencies = tokenList.map(CryptoCurrencyStatus::currency),
)
val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest
availablePairsByNetworkFlow.update {
it.toMutableMap().apply {
put(initialCurrency, pairs)
}
}
updateAvailablePairs(networkInfo = networkInfo, statuses = statuses)
}
}
}
private suspend fun updateAvailablePairs(networkInfo: LeastTokenInfo, statuses: List<CryptoCurrencyStatus>) {
runCatching {
availablePairsByNetworkFlow.update(networkInfo = networkInfo, state = lceLoading())
getAvailablePairsUseCase(
initialCurrency = networkInfo,
currencies = statuses.map(CryptoCurrencyStatus::currency),
)
}
.onSuccess { pairs ->
availablePairsByNetworkFlow.update(networkInfo = networkInfo, state = pairs.lceContent())
}
.onFailure { cause ->
availablePairsByNetworkFlow.update(networkInfo = networkInfo, state = cause.lceError())
}
}
private fun MutableStateFlow<Map<LeastTokenInfo, AvailablePairsState>>.update(
networkInfo: LeastTokenInfo,
state: AvailablePairsState,
) {
update {
it.toMutableMap().apply {
put(networkInfo, state)
}
}
}
private fun getAppCurrencyAndBalanceHidingFlow(): Flow<Pair<AppCurrency, Boolean>> {
return combine(
flow = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(),
@ -186,7 +277,8 @@ internal class AvailableSwapPairsModel @Inject constructor(
isAvailable &&
status.value !is CryptoCurrencyStatus.MissedDerivation &&
status.value !is CryptoCurrencyStatus.Unreachable
status.value !is CryptoCurrencyStatus.Unreachable &&
!status.currency.isCustom
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.onramp.tokenlist.entity
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import kotlinx.collections.immutable.ImmutableList
@ -19,4 +20,5 @@ internal data class TokenListUM(
val availableItems: ImmutableList<TokensListItemUM>,
val unavailableItems: ImmutableList<TokensListItemUM>,
val isBalanceHidden: Boolean,
val warning: NotificationUM? = null,
)

View file

@ -1,22 +1,18 @@
package com.tangem.features.onramp.tokenlist.entity.transformer
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedCryptoAmount
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedFiatAmount
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import kotlinx.collections.immutable.toImmutableList
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
import com.tangem.features.onramp.tokenlist.entity.utils.addHeader
@Suppress("LongParameterList")
internal class UpdateTokenItemsTransformer(
private val appCurrency: AppCurrency,
private val onItemClick: (TokenItemState, CryptoCurrencyStatus) -> Unit,
@ -27,36 +23,22 @@ internal class UpdateTokenItemsTransformer(
override fun transform(prevState: TokenListUM): TokenListUM {
val availableItems = convertStatuses(
converter = createAvailableTokenItemStateConverter(),
converter = OnrampTokenItemStateConverterFactory.createAvailableItemConverter(appCurrency, onItemClick),
statuses = statuses[true].orEmpty(),
)
val unavailableItems = convertStatuses(
converter = createUnavailableTokenItemStateConverter(),
converter = OnrampTokenItemStateConverterFactory.createUnavailableItemConverter(appCurrency),
statuses = statuses[false].orEmpty(),
)
return prevState.copy(
availableItems = buildList {
if (availableItems.isNotEmpty()) {
createGroupTitle(
textReference = resourceReference(id = R.string.exchange_tokens_available_tokens_header),
)
.let(::add)
}
addAll(availableItems)
}
.toImmutableList(),
unavailableItems = buildList {
if (unavailableItems.isNotEmpty()) {
createGroupTitle(textReference = unavailableTokensHeaderReference).let(::add)
}
addAll(unavailableItems)
}
.toImmutableList(),
availableItems = availableItems.addHeader(
textReference = resourceReference(id = R.string.exchange_tokens_available_tokens_header),
),
unavailableItems = unavailableItems.addHeader(textReference = unavailableTokensHeaderReference),
isBalanceHidden = isBalanceHidden,
warning = null,
)
}
@ -67,88 +49,4 @@ internal class UpdateTokenItemsTransformer(
return converter.convertList(statuses)
.map(TokensListItemUM::Token)
}
private fun createAvailableTokenItemStateConverter(): TokenItemStateConverter {
return TokenItemStateConverter(
appCurrency = appCurrency,
subtitleStateProvider = { createSubtitleState(status = it, isAvailable = true) },
subtitle2StateProvider = ::createSubtitle2State,
fiatAmountStateProvider = { createFiatAmountStateProvider(status = it, isAvailable = true) },
onItemClick = onItemClick,
)
}
private fun createUnavailableTokenItemStateConverter(): TokenItemStateConverter {
return TokenItemStateConverter(
appCurrency = appCurrency,
iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) },
titleStateProvider = {
TokenItemState.TitleState.Content(
text = stringReference(value = it.currency.name),
isAvailable = false,
)
},
subtitleStateProvider = { createSubtitleState(status = it, isAvailable = false) },
subtitle2StateProvider = ::createSubtitle2State,
fiatAmountStateProvider = { createFiatAmountStateProvider(status = it, isAvailable = false) },
)
}
private fun createSubtitleState(status: CryptoCurrencyStatus, isAvailable: Boolean): TokenItemState.SubtitleState {
return when (status.value) {
CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading
else -> {
TokenItemState.SubtitleState.TextContent(
value = stringReference(value = status.currency.symbol),
isAvailable = isAvailable,
)
}
}
}
private fun createSubtitle2State(status: CryptoCurrencyStatus): TokenItemState.Subtitle2State? {
return when (status.value) {
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> {
TokenItemState.Subtitle2State.TextContent(
text = status.getFormattedCryptoAmount(includeStaking = false),
)
}
is CryptoCurrencyStatus.Loading,
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> null
}
}
private fun createFiatAmountStateProvider(
status: CryptoCurrencyStatus,
isAvailable: Boolean,
): TokenItemState.FiatAmountState? {
return when (status.value) {
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> {
TokenItemState.FiatAmountState.TextContent(
text = status.getFormattedFiatAmount(appCurrency = appCurrency, includeStaking = false),
isAvailable = isAvailable,
)
}
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Loading,
-> null
}
}
private fun createGroupTitle(textReference: TextReference): TokensListItemUM.GroupTitle {
return TokensListItemUM.GroupTitle(id = textReference.hashCode(), text = textReference)
}
}

View file

@ -0,0 +1,104 @@
package com.tangem.features.onramp.tokenlist.entity.utils
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedCryptoAmount
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedFiatAmount
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
/**
[REDACTED_AUTHOR]
*/
internal object OnrampTokenItemStateConverterFactory {
fun createAvailableItemConverter(
appCurrency: AppCurrency,
onItemClick: (TokenItemState, CryptoCurrencyStatus) -> Unit,
): TokenItemStateConverter {
return TokenItemStateConverter(
appCurrency = appCurrency,
subtitleStateProvider = { createSubtitleState(status = it, isAvailable = true) },
subtitle2StateProvider = ::createSubtitle2State,
fiatAmountStateProvider = {
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = true)
},
onItemClick = onItemClick,
)
}
fun createUnavailableItemConverter(appCurrency: AppCurrency): TokenItemStateConverter {
return TokenItemStateConverter(
appCurrency = appCurrency,
iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) },
titleStateProvider = {
TokenItemState.TitleState.Content(
text = stringReference(value = it.currency.name),
isAvailable = false,
)
},
subtitleStateProvider = { createSubtitleState(status = it, isAvailable = false) },
subtitle2StateProvider = ::createSubtitle2State,
fiatAmountStateProvider = {
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = false)
},
)
}
private fun createSubtitleState(status: CryptoCurrencyStatus, isAvailable: Boolean): TokenItemState.SubtitleState {
return when (status.value) {
CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading
else -> {
TokenItemState.SubtitleState.TextContent(
value = stringReference(value = status.currency.symbol),
isAvailable = isAvailable,
)
}
}
}
private fun createSubtitle2State(status: CryptoCurrencyStatus): TokenItemState.Subtitle2State? {
return when (status.value) {
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> {
TokenItemState.Subtitle2State.TextContent(
text = status.getFormattedCryptoAmount(includeStaking = false),
)
}
is CryptoCurrencyStatus.Loading,
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> null
}
}
private fun createFiatAmountStateProvider(
status: CryptoCurrencyStatus,
appCurrency: AppCurrency,
isAvailable: Boolean,
): TokenItemState.FiatAmountState? {
return when (status.value) {
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> {
TokenItemState.FiatAmountState.TextContent(
text = status.getFormattedFiatAmount(appCurrency = appCurrency, includeStaking = false),
isAvailable = isAvailable,
)
}
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Loading,
-> null
}
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.onramp.tokenlist.entity.utils
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
fun List<TokensListItemUM>.addHeader(textReference: TextReference): ImmutableList<TokensListItemUM> {
val items = this@addHeader
return buildList {
if (items.isNotEmpty()) {
createGroupTitle(textReference = textReference).let(::add)
}
addAll(items)
}
.toImmutableList()
}
private fun createGroupTitle(textReference: TextReference): TokensListItemUM.GroupTitle {
return TokensListItemUM.GroupTitle(id = textReference.hashCode(), text = textReference)
}

View file

@ -14,9 +14,11 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.tokenlist.TokenListItem
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
@ -37,15 +39,29 @@ import kotlinx.collections.immutable.ImmutableList
@Composable
internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) {
Column(modifier) {
SearchBar(searchBarUM = state.searchBarUM)
if (state.warning == null) {
SearchBar(searchBarUM = state.searchBarUM)
} else {
when (state.warning) {
is NotificationUM.Warning.OnrampErrorNotification -> {
Notification(config = state.warning.config, containerColor = TangemTheme.colors.background.primary)
}
is NotificationUM.Warning.SwapNoAvailablePair -> {
Notification(config = state.warning.config, containerColor = TangemTheme.colors.button.disabled)
}
else -> Unit
}
}
SpacerH12()
if (state.availableItems.isNotEmpty()) {
SpacerH12()
ItemsBlock(items = state.availableItems, isBalanceHidden = state.isBalanceHidden)
}
ItemsBlock(items = state.availableItems, isBalanceHidden = state.isBalanceHidden)
SpacerH12()
ItemsBlock(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden)
if (state.unavailableItems.isNotEmpty()) {
SpacerH12()
ItemsBlock(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden)
}
}
}

View file

@ -1,52 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.exchange.RampStateManager
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.UpdateMultiWalletActionsTransformer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onEach
/**
* Multi-currency wallet subscriber for actions state updating
*
* @property userWallet user wallet
* @property rampStateManager ramp state manager
* @property stateController state controller
*
[REDACTED_AUTHOR]
*/
internal class MultiWalletActionsSubscriber(
private val userWallet: UserWallet,
private val rampStateManager: RampStateManager,
private val stateController: WalletStateController,
) : WalletSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<*> {
return combine(
flow = rampStateManager.getBuyInitializationStatus(),
flow2 = rampStateManager.getSellInitializationStatus(),
flow3 = rampStateManager.getSwapInitializationStatus(userWalletId = userWallet.walletId),
transform = ::RampStatuses,
)
.onEach { statuses ->
stateController.update(
UpdateMultiWalletActionsTransformer(
userWalletId = userWallet.walletId,
buyStatus = statuses.buy,
sellStatus = statuses.sell,
swapStatus = statuses.swap,
),
)
}
}
private data class RampStatuses(
val buy: Lce<Throwable, Any>,
val sell: Lce<Throwable, Any>,
val swap: Lce<Throwable, Any>,
)
}

View file

@ -18,6 +18,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetRefreshStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListErrorTransformer
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.CoroutineScope
@ -46,6 +47,7 @@ internal class WalletClickIntents @Inject constructor(
private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview,
private val rampStateManager: RampStateManager,
private val dispatchers: CoroutineDispatcherProvider,
private val onrampFeatureToggles: OnrampFeatureToggles,
) : BaseWalletClickIntents(),
WalletCardClickIntents by walletCardClickIntentsImplementor,
WalletWarningsClickIntents by warningsClickIntentsImplementer,
@ -120,10 +122,13 @@ internal class WalletClickIntents @Inject constructor(
fetchTokenListUseCase(userWalletId = userWallet.walletId, mode = RefreshMode.FULL)
}
listOf(
async { rampStateManager.fetchBuyServiceData() },
async { rampStateManager.fetchSellServiceData() },
)
buildList {
if (!onrampFeatureToggles.isFeatureEnabled) {
async { rampStateManager.fetchBuyServiceData() }.let(::add)
}
async { rampStateManager.fetchSellServiceData() }.let(::add)
}
.awaitAll()
maybeFetchResult.onLeft {

View file

@ -48,6 +48,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.collections.immutable.toImmutableList
@ -119,6 +120,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private val appRouter: AppRouter,
private val rampStateManager: RampStateManager,
private val getUserCountryUseCase: GetUserCountryUseCase,
private val onrampFeatureToggles: OnrampFeatureToggles,
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
override fun onSendClick(
@ -493,7 +495,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
onMultiWalletActionClick(
statusFlow = rampStateManager.getSwapInitializationStatus(userWalletId),
statusFlow = rampStateManager.getExpressInitializationStatus(userWalletId),
route = AppRoute.SwapCrypto(userWalletId = userWalletId),
eventCreator = MainScreenAnalyticsEvent::ButtonSwap,
)
@ -501,7 +503,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
override fun onMultiWalletBuyClick(userWalletId: UserWalletId) {
onMultiWalletActionClick(
statusFlow = rampStateManager.getBuyInitializationStatus(),
statusFlow = if (onrampFeatureToggles.isFeatureEnabled) {
rampStateManager.getExpressInitializationStatus(userWalletId)
} else {
rampStateManager.getBuyInitializationStatus()
},
route = AppRoute.BuyCrypto(userWalletId = userWalletId),
eventCreator = MainScreenAnalyticsEvent::ButtonBuy,
)