From 207fe025865f20bdf2a2a5702e1e79da2ccdbafb Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Dec 2024 16:48:30 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../exchangeServices/DefaultRampManager.kt | 51 +++-- .../common/ui/notifications/NotificationUM.kt | 5 + .../domain/exchange/RampStateManager.kt | 2 +- .../SetErrorWarningTransformer.kt | 27 +++ .../SetLoadingTokenItemsTransformer.kt | 3 + .../SetNoAvailablePairsTransformer.kt | 36 +++ .../model/AvailableSwapPairsModel.kt | 208 +++++++++++++----- .../onramp/tokenlist/entity/TokenListUM.kt | 2 + .../UpdateTokenItemsTransformer.kt | 120 +--------- .../OnrampTokenItemStateConverterFactory.kt | 104 +++++++++ .../entity/utils/OnrampTokenListUMExt.kt | 23 ++ .../onramp/tokenlist/ui/OnrampTokenList.kt | 30 ++- .../MultiWalletActionsSubscriber.kt | 52 ----- .../viewmodels/intents/WalletClickIntents.kt | 13 +- .../WalletCurrencyActionsClickIntents.kt | 10 +- 15 files changed, 433 insertions(+), 253 deletions(-) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetErrorWarningTransformer.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenListUMExt.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionsSubscriber.kt diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index 46c87ba9b5..7f8f63e344 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -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 { @@ -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 { + override fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow { return expressServiceLoader.getInitializationStatus(userWalletId) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index 620038bf9a..9f5bf56e7d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -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( diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt index 8e4928a02d..0adb568906 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -39,5 +39,5 @@ interface RampStateManager { fun getSellInitializationStatus(): Flow> - fun getSwapInitializationStatus(userWalletId: UserWalletId): Flow> + fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow> } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetErrorWarningTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetErrorWarningTransformer.kt new file mode 100644 index 0000000000..d3410bd2dc --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetErrorWarningTransformer.kt @@ -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, + ), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt index b14b3a023e..b1a335134f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt @@ -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, ) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt new file mode 100644 index 0000000000..7e9c537c9e --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt @@ -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, + 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, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index cf2d9797ed..d31bb7513d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -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> + @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>>(emptyMap()) + private val availablePairsByNetworkFlow = MutableStateFlow>(emptyMap()) init { + initializeSearchBardCallbacks() + + subscribeOnUpdateState() + subscribeOnAvailablePairsUpdates() + } + + private fun getTokenListUseCaseFlow(): SharedFlow> { + 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, + currencies: List, + selectedStatus: CryptoCurrencyStatus?, + query: String, + availablePairs: List, + ): 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, + ): 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) { + 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>.update( + networkInfo: LeastTokenInfo, + state: AvailablePairsState, + ) { + update { + it.toMutableMap().apply { + put(networkInfo, state) + } + } + } + private fun getAppCurrencyAndBalanceHidingFlow(): Flow> { 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 } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt index d941f4f61d..615823f4e4 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt @@ -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, val unavailableItems: ImmutableList, val isBalanceHidden: Boolean, + val warning: NotificationUM? = null, ) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt index 66f2119526..2913ce4223 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt @@ -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) - } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt new file mode 100644 index 0000000000..e48ddc2674 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenListUMExt.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenListUMExt.kt new file mode 100644 index 0000000000..27c3500039 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenListUMExt.kt @@ -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.addHeader(textReference: TextReference): ImmutableList { + 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) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index e39e10eff6..069b27e874 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -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) + } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionsSubscriber.kt deleted file mode 100644 index 266bb7b67c..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionsSubscriber.kt +++ /dev/null @@ -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, - val sell: Lce, - val swap: Lce, - ) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index f40ea9b059..786be57b6c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -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 { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 85cd2a7a03..3b15d7ee6c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -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, )