From c6c7cf61ab18da6fe60cf62a613d1502e453d3b6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 15:49:08 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../common/ui/markets/MarketListItemV2.kt | 2 + .../onramp/analytics/OnrampAnalyticsEvent.kt | 14 +++ .../tokens/actions/BaseActionsFactory.kt | 31 ------ .../tokens/actions/CommonActionsFactory.kt | 10 +- .../actions/OutdatedDataActionsFactory.kt | 10 +- .../actions/UnreachableActionsFactory.kt | 10 +- features/onramp/impl/build.gradle.kts | 4 + .../main/entity/OnrampMainComponentUM.kt | 5 + .../main/entity/factory/OnrampStateFactory.kt | 47 ++++++++- .../main/model/OnrampMainComponentModel.kt | 68 ++++++++++--- .../onramp/main/ui/OnrampAmountContent.kt | 4 +- .../main/ui/OnrampMainComponentContent.kt | 15 ++- .../tokenlist/model/OnrampTokenListModel.kt | 16 +--- .../entity/factory/OnrampStateFactoryTest.kt | 95 +++++++++++++++++++ 14 files changed, 241 insertions(+), 90 deletions(-) create mode 100644 features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactoryTest.kt diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItemV2.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItemV2.kt index 985380f91c..ba70d244ec 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItemV2.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItemV2.kt @@ -3,6 +3,7 @@ package com.tangem.common.ui.markets import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -73,6 +74,7 @@ fun MarketListItemContentV2(model: MarketsListItemUM, modifier: Modifier = Modif tangemIconUM = TangemIconUM.Url(model.iconUrl, fallbackRes = R.drawable.ic_custom_token_44), modifier = Modifier .size(TangemTheme.dimens2.x10) + .clip(RoundedCornerShape(8.dp)) .layoutId(layoutId = TangemRowLayoutId.HEAD), ) diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt index 5010202433..1711ccdcbd 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt @@ -1,6 +1,7 @@ package com.tangem.domain.onramp.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_DESCRIPTION import com.tangem.core.analytics.models.AnalyticsParam.Key.PAYMENT_METHOD @@ -208,4 +209,17 @@ sealed class OnrampAnalyticsEvent( event = "Button - All Offers", params = emptyMap(), ) + + data class NoticeBuyNotSupported( + private val source: OnrampSource, + private val tokenSymbol: String, + private val blockchain: String, + ) : OnrampAnalyticsEvent( + event = "Notice - Buy Not Supported", + params = mapOf( + SOURCE to source.analyticsName, + TOKEN_PARAM to tokenSymbol, + BLOCKCHAIN to blockchain, + ), + ) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt index 6c821b7893..4cc9fc0ed5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt @@ -54,37 +54,6 @@ internal open class BaseActionsFactory( } } - /** - * Determines the unavailability reason for the BUY action - * - * @param userWallet the user's cold wallet - * @param currency the cryptocurrency to check - * @param requirementsDeferred a deferred object containing the asset requirements condition - */ - protected suspend fun getOnrampUnavailabilityReason( - userWallet: UserWallet, - currency: CryptoCurrency, - requirementsDeferred: Deferred?, - ): ScenarioUnavailabilityReason { - // Start2Coin (S2C) are legacy single-currency cards that do not support buying crypto in-app - // (historically only Receive/Send were offered for them). - if (userWallet is UserWallet.Cold && userWallet.cardTypesResolver.isStart2Coin()) { - return ScenarioUnavailabilityReason.BuyUnavailable(currency.name) - } - - val onrampUnavailabilityReason = rampStateManager.availableForBuy( - userWallet = userWallet, - cryptoCurrency = currency, - ) - val shouldCheckAssetRequirements = - onrampUnavailabilityReason == ScenarioUnavailabilityReason.None && requirementsDeferred != null - return if (shouldCheckAssetRequirements) { - getReceiveScenario(requirementsDeferred.await()) - } else { - onrampUnavailabilityReason - } - } - /** * Determines the unavailability reason for the SEND action * diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt index f158738630..f244724fd4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt @@ -50,14 +50,6 @@ internal class CommonActionsFactory( null } - val onrampUnavailabilityReasonDeferred = async { - getOnrampUnavailabilityReason( - userWallet = userWallet, - currency = cryptoCurrencyStatus.currency, - requirementsDeferred = requirementsDeferred, - ) - } - val sendUnavailabilityReasonDeferred = async { getSendUnavailabilityReason(userWalletId = userWallet.walletId, cryptoCurrencyStatus = cryptoCurrencyStatus) } @@ -99,7 +91,7 @@ internal class CommonActionsFactory( // endregion // region Buy - addBuyAction(reason = onrampUnavailabilityReasonDeferred.await()) + addBuyAction(reason = ScenarioUnavailabilityReason.None) // endregion // region Sell diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt index f432fc0495..111e4685a5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt @@ -52,14 +52,6 @@ internal class OutdatedDataActionsFactory( null } - val onrampUnavailabilityReasonDeferred = async { - getOnrampUnavailabilityReason( - userWallet = userWallet, - currency = cryptoCurrencyStatus.currency, - requirementsDeferred = requirementsDeferred, - ) - } - val sendUnavailabilityReasonDeferred = if (sources.networkSource == StatusSource.ACTUAL) { async { getSendUnavailabilityReason( @@ -87,7 +79,7 @@ internal class OutdatedDataActionsFactory( // endregion // region Buy - addBuyAction(reason = onrampUnavailabilityReasonDeferred.await()) + addBuyAction(reason = ScenarioUnavailabilityReason.None) // endregion // region Stake diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt index 11a8155d1d..23e188e3dd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt @@ -35,14 +35,6 @@ internal class UnreachableActionsFactory( null } - val onrampUnavailabilityReasonDeferred = async { - getOnrampUnavailabilityReason( - userWallet = userWallet, - currency = cryptoCurrencyStatus.currency, - requirementsDeferred = requirementsDeferred, - ) - } - val hideTokenUnavailabilityReason = getTokenHideUnavailabilityReason(userWallet) // endregion @@ -52,7 +44,7 @@ internal class UnreachableActionsFactory( // endregion // region Buy - addBuyAction(reason = onrampUnavailabilityReasonDeferred.await()) + addBuyAction(reason = ScenarioUnavailabilityReason.None) // endregion // region Receive diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 87252298e9..6a21e2425b 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -82,4 +82,8 @@ dependencies { /** Other */ implementation(deps.decompose.ext.compose) implementation(deps.kotlin.immutable.collections) + + /** Tests */ + testImplementation(projects.test.core) + testImplementation(projects.common.test) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt index b9e86bee09..07967d59c2 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt @@ -3,6 +3,7 @@ package com.tangem.features.onramp.main.entity import androidx.compose.runtime.Immutable import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.ds.message.TangemMessageUM import com.tangem.core.ui.extensions.TextReference @Immutable @@ -11,9 +12,12 @@ internal sealed interface OnrampMainComponentUM { val topBarConfig: OnrampMainTopBarUM val errorNotification: NotificationUM? + val buyNotSupportedMessage: TangemMessageUM? + data class InitialLoading( override val topBarConfig: OnrampMainTopBarUM, override val errorNotification: NotificationUM?, + override val buyNotSupportedMessage: TangemMessageUM? = null, ) : OnrampMainComponentUM data class Content( @@ -22,6 +26,7 @@ internal sealed interface OnrampMainComponentUM { val amountBlockState: OnrampAmountBlockUM, val offersBlockState: OnrampOffersBlockUM, val onrampAmountButtonUMState: OnrampAmountButtonUMState, + override val buyNotSupportedMessage: TangemMessageUM? = null, ) : OnrampMainComponentUM } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index 46e69fe061..132bb8aa5b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -7,10 +7,12 @@ import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.core.ui.ds.message.TangemMessageIconPosition +import com.tangem.core.ui.ds.message.TangemMessageUM +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.error.OnrampError @@ -21,6 +23,7 @@ import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.main.entity.* import com.tangem.utils.Provider import java.math.BigDecimal +import com.tangem.core.ui.R as CoreUiR internal class OnrampStateFactory( private val currentStateProvider: Provider, @@ -120,6 +123,42 @@ internal class OnrampStateFactory( } } + fun getBuyNotSupportedState(state: OnrampMainComponentUM = currentStateProvider()): OnrampMainComponentUM { + val message = buildBuyNotSupportedMessage() + + return when (state) { + is OnrampMainComponentUM.Content -> state.copy( + buyNotSupportedMessage = message, + errorNotification = null, + offersBlockState = OnrampOffersBlockUM.Empty, + onrampAmountButtonUMState = OnrampAmountButtonUMState.None, + amountBlockState = state.amountBlockState.copy( + amountFieldModel = state.amountBlockState.amountFieldModel.copy(isError = true), + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, + ), + ) + is OnrampMainComponentUM.InitialLoading -> state.copy( + buyNotSupportedMessage = message, + errorNotification = null, + ) + } + } + + private fun buildBuyNotSupportedMessage(): TangemMessageUM = TangemMessageUM( + id = "buy_not_supported", + title = resourceReference( + id = R.string.onramp_token_is_not_supported_banner_title, + formatArgs = wrappedList(cryptoCurrency.name), + ), + subtitle = resourceReference(R.string.onramp_token_is_not_supported_banner_subtitle), + messageEffect = TangemMessageEffect.None, + iconUM = TangemIconUM.Icon( + iconRes = CoreUiR.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + iconPosition = TangemMessageIconPosition.Leading, + ) + private fun getNoPairsErrorState(): OnrampMainComponentUM { val state = currentStateProvider() val contentState = state as? OnrampMainComponentUM.Content ?: return state diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 86098da81b..5c85ec5a96 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.onramp.main.model import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -9,12 +10,15 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.fields.InputManager import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.onramp.* import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent import com.tangem.domain.onramp.model.OnrampAvailability +import com.tangem.domain.onramp.model.OnrampCountry import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.domain.onramp.model.error.OnrampError +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.main.OnrampMainComponent import com.tangem.features.onramp.main.entity.* @@ -30,9 +34,9 @@ import com.tangem.utils.coroutines.PeriodicTask import com.tangem.utils.coroutines.SingleTaskScheduler import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.isNullOrZero +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -46,6 +50,7 @@ internal class OnrampMainComponentModel @Inject constructor( private val fetchQuotesUseCase: OnrampFetchQuotesUseCase, private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase, private val fetchPairsUseCase: OnrampFetchPairsUseCase, + private val rampStateManager: RampStateManager, private val amountInputManager: InputManager, private val getOnrampOffersUseCase: GetOnrampOffersUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, @@ -222,6 +227,8 @@ internal class OnrampMainComponentModel @Inject constructor( } private fun handleOnrampAvailability(availability: OnrampAvailability) { + // "Buy not supported" notification has priority over the residency flow. + if (state.value.buyNotSupportedMessage != null) return when (availability) { is OnrampAvailability.Available -> Unit is OnrampAvailability.ConfirmResidency, @@ -274,23 +281,30 @@ internal class OnrampMainComponentModel @Inject constructor( ifLeft = ::handleOnrampError, ifRight = { country -> if (country == null) return@onEach - state.update { prevState -> - when (prevState) { - is OnrampMainComponentUM.Content -> { - amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) - } - is OnrampMainComponentUM.InitialLoading -> { - stateFactory.getReadyState(country.defaultCurrency, params.initialFiatAmount) - } - } + // Resolve token-level buy support BEFORE emitting any Content state, so an + // unsupported token never briefly shows an enabled amount field — otherwise it + // would grab focus and flash the keyboard before being disabled. + if (isTokenNotSupportedForBuy()) { + showBuyNotSupported(country) + } else { + state.update { prevState -> getCountryUpdatedState(prevState, country) } + updatePairsAndQuotes() } - updatePairsAndQuotes() }, ) } .launchIn(modelScope) } + private fun getCountryUpdatedState( + prevState: OnrampMainComponentUM, + country: OnrampCountry, + ): OnrampMainComponentUM = when (prevState) { + is OnrampMainComponentUM.Content -> amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) + is OnrampMainComponentUM.InitialLoading -> + stateFactory.getReadyState(country.defaultCurrency, params.initialFiatAmount) + } + private fun subscribeToQuotesUpdate() { getOnrampQuotesUseCase.invoke() .conflate() @@ -356,11 +370,43 @@ internal class OnrampMainComponentModel @Inject constructor( ) } + private suspend fun isTokenNotSupportedForBuy(): Boolean { + // Token-level "cannot be bought", independent of country: the asset is either flagged as + // not onrampable (BuyUnavailable) or absent from the express asset list (AssetNotFound). + // Transient express states (loading/unreachable) are NOT treated as "not supported". + val reason = rampStateManager.availableForBuy( + userWallet = userWallet, + cryptoCurrency = params.cryptoCurrency, + ) + return reason is ScenarioUnavailabilityReason.BuyUnavailable || + reason is ScenarioUnavailabilityReason.AssetNotFound + } + private fun handleOnrampError(onrampError: OnrampError) { TangemLogger.e(onrampError.toString()) state.update { stateFactory.getOnrampErrorState(onrampError) } } + private fun showBuyNotSupported(country: OnrampCountry) { + if (state.value.buyNotSupportedMessage != null) return + + analyticsEventHandler.send( + OnrampAnalyticsEvent.NoticeBuyNotSupported( + source = params.source, + tokenSymbol = params.cryptoCurrency.symbol, + blockchain = params.cryptoCurrency.network.name, + ), + ) + quotesTaskScheduler.cancelTask() + // "Not supported" has priority: hide the residency bottom sheet if it was already shown. + bottomSheetNavigation.dismiss() + // Emit the not-supported state in a single update built from the ready state, so the amount + // field never appears enabled first (no focus/keyboard flash). + state.update { prevState -> + stateFactory.getBuyNotSupportedState(getCountryUpdatedState(prevState, country)) + } + } + private fun sendOnrampQuotesErrorAnalytic(quotes: List) { quotes.forEach { errorState -> when (errorState) { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt index e0cedd00a0..5afa3dab84 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt @@ -125,7 +125,9 @@ private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: Strin ) LaunchedEffect(key1 = Unit) { - requester.requestFocus() + if (!amountField.isError) { + requester.requestFocus() + } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt index 2b24ad2fb9..2c61db8413 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.ds.message.TangemMessage import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero @@ -75,7 +76,7 @@ private fun InitialLoading(state: OnrampMainComponentUM.InitialLoading, modifier verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { OnrampAmountContentLoading() - if (state.errorNotification != null) Notification(config = state.errorNotification.config) + OnrampNotifications(state = state) } } @@ -134,6 +135,16 @@ private fun Content(state: OnrampMainComponentUM.Content, modifier: Modifier = M OnrampOffersContent(state = state.offersBlockState) - if (state.errorNotification != null) Notification(config = state.errorNotification.config) + OnrampNotifications(state = state) + } +} + +@Composable +private fun OnrampNotifications(state: OnrampMainComponentUM, modifier: Modifier = Modifier) { + val buyNotSupportedMessage = state.buyNotSupportedMessage + val errorNotification = state.errorNotification + when { + buyNotSupportedMessage != null -> TangemMessage(messageUM = buyNotSupportedMessage, modifier = modifier) + errorNotification != null -> Notification(config = errorNotification.config, modifier = modifier) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index b11f85dce7..18210052a3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -23,7 +23,6 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.tokens.GetAssetRequirementsUseCase -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM @@ -270,9 +269,7 @@ internal class OnrampTokenListModel @Inject constructor( val isNotUnreachable = status.value !is CryptoCurrencyStatus.Unreachable val isAvailable = when (params.filterOperation) { - OnrampOperation.BUY -> { - isAvailableForBuy - } // unreachable state is available for Buy operation + OnrampOperation.BUY -> true OnrampOperation.SELL -> isNotUnreachable OnrampOperation.SWAP -> { isNotUnreachable && isAvailableForBuy @@ -295,12 +292,7 @@ internal class OnrampTokenListModel @Inject constructor( private suspend fun checkAvailabilityByOperation(status: CryptoCurrencyStatus): Boolean { return when (params.filterOperation) { - OnrampOperation.BUY -> { - rampStateManager.availableForBuy( - userWallet = userWallet, - cryptoCurrency = status.currency, - ).isAvailable() - } + OnrampOperation.BUY -> true OnrampOperation.SELL -> { rampStateManager.availableForSell( userWalletId = userWallet.walletId, @@ -318,8 +310,4 @@ internal class OnrampTokenListModel @Inject constructor( } } } - - private fun ScenarioUnavailabilityReason.isAvailable(): Boolean { - return this == ScenarioUnavailabilityReason.None - } } \ No newline at end of file diff --git a/features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactoryTest.kt b/features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactoryTest.kt new file mode 100644 index 0000000000..e5beb49d45 --- /dev/null +++ b/features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactoryTest.kt @@ -0,0 +1,95 @@ +package com.tangem.features.onramp.main.entity.factory + +import com.google.common.truth.Truth.assertThat +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.main.entity.OnrampAmountButtonUMState +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.features.onramp.main.entity.OnrampOffersBlockUM +import com.tangem.features.onramp.main.entity.OnrampSecondaryFieldErrorUM +import com.tangem.utils.Provider +import io.mockk.mockk +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test + +internal class OnrampStateFactoryTest { + + private lateinit var currentState: OnrampMainComponentUM + + private val cryptoCurrency = MockCryptoCurrencyFactory().ethereum + + private val factory = OnrampStateFactory( + currentStateProvider = Provider { currentState }, + onrampAmountButtonUMStateFactory = OnrampAmountButtonUMStateFactory(), + cryptoCurrency = cryptoCurrency, + onrampIntents = mockk(relaxed = true), + ) + + @Test + fun `GIVEN initial loading with error WHEN getBuyNotSupportedState THEN shows None message and clears error`() { + // Arrange + currentState = factory.getInitialState(currency = "BTC", onClose = {}, openSettings = {}) + .copy(errorNotification = mockk()) + + // Act + val result = factory.getBuyNotSupportedState() + + // Assert + val message = result.buyNotSupportedMessage + assertThat(message).isNotNull() + assertThat(message!!.messageEffect).isEqualTo(TangemMessageEffect.None) + assertThat(message.title).isEqualTo( + resourceReference( + id = R.string.onramp_token_is_not_supported_banner_title, + formatArgs = wrappedList(cryptoCurrency.name), + ), + ) + assertThat(message.subtitle).isEqualTo( + resourceReference(R.string.onramp_token_is_not_supported_banner_subtitle), + ) + assertThat(result.errorNotification).isNull() + } + + @Test + fun `GIVEN content with errors WHEN getBuyNotSupportedState THEN message has priority and other errors hidden`() { + // Arrange + currentState = factory.getInitialState(currency = "BTC", onClose = {}, openSettings = {}) + val content = factory.getReadyState(currency = USD_CURRENCY) as OnrampMainComponentUM.Content + currentState = content.copy( + errorNotification = mockk(), + offersBlockState = OnrampOffersBlockUM.Loading, + onrampAmountButtonUMState = OnrampAmountButtonUMState.Loaded(persistentListOf()), + amountBlockState = content.amountBlockState.copy( + amountFieldModel = content.amountBlockState.amountFieldModel.copy(isError = true), + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Error(stringReference("error")), + ), + ) + + // Act + val result = factory.getBuyNotSupportedState() as OnrampMainComponentUM.Content + + // Assert + assertThat(result.buyNotSupportedMessage).isNotNull() + assertThat(result.errorNotification).isNull() + assertThat(result.offersBlockState).isEqualTo(OnrampOffersBlockUM.Empty) + assertThat(result.onrampAmountButtonUMState).isEqualTo(OnrampAmountButtonUMState.None) + assertThat(result.amountBlockState.secondaryFieldModel).isEqualTo(OnrampSecondaryFieldErrorUM.Empty) + // Amount input is locked (disabled via isError) — like the unsupported-country case. + assertThat(result.amountBlockState.amountFieldModel.isError).isTrue() + } + + private companion object { + val USD_CURRENCY = OnrampCurrency( + name = "US Dollar", + code = "USD", + image = null, + precision = 2, + unit = "$", + ) + } +} \ No newline at end of file