Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-16 09:36:46 +03:00
parent cf182832db
commit ae979f60cd
11 changed files with 110 additions and 65 deletions

View file

@ -237,7 +237,7 @@ object OnboardingHelper {
scope.launch {
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
val isRussia = getUserCountryCodeUseCase().isRight { it is UserCountry.Russia }
val isRussia = getUserCountryCodeUseCase.invokeSync().isRight { it is UserCountry.Russia }
val onrampFeatureToggles = store.inject(DaggerGraphState::onrampFeatureToggles)
if (isRussia && !onrampFeatureToggles.isFeatureEnabled) {

View file

@ -92,7 +92,7 @@ object TradeCryptoMiddleware {
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
val onrampFeatureToggles = store.inject(DaggerGraphState::onrampFeatureToggles)
val isRussia = getUserCountryCodeUseCase().isRight { it is UserCountry.Russia }
val isRussia = getUserCountryCodeUseCase.invokeSync().isRight { it is UserCountry.Russia }
if (action.checkUserLocation && isRussia && !onrampFeatureToggles.isFeatureEnabled) {
val dialogData = topUrl?.let {

View file

@ -245,6 +245,16 @@ sealed class NotificationUM(val config: NotificationConfig) {
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),
)
data object SellingRegionalRestriction : Warning(
title = resourceReference(id = R.string.selling_regional_restriction_alert_title),
subtitle = resourceReference(id = R.string.selling_regional_restriction_alert_message),
)
data object InsufficientBalanceForSelling : Warning(
title = resourceReference(id = R.string.selling_insufficient_balance_alert_title),
subtitle = resourceReference(id = R.string.selling_insufficient_balance_alert_message),
)
}
open class Info(

View file

@ -12,6 +12,7 @@ import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
@ -127,6 +128,8 @@ internal class DefaultSettingsRepository(
return null
}
override fun getUserCountryCode(): StateFlow<UserCountry?> = userCountryFlow
override suspend fun fetchUserCountryCode() {
Timber.i("Start fetching user country code")

View file

@ -1,6 +1,7 @@
package com.tangem.domain.settings.repositories
import com.tangem.domain.settings.usercountry.models.UserCountry
import kotlinx.coroutines.flow.StateFlow
@Suppress("TooManyFunctions")
interface SettingsRepository {
@ -39,6 +40,8 @@ interface SettingsRepository {
suspend fun getUserCountryCodeSync(): UserCountry?
fun getUserCountryCode(): StateFlow<UserCountry?>
suspend fun fetchUserCountryCode()
suspend fun setGoogleServicesAvailability(value: Boolean)

View file

@ -7,6 +7,8 @@ import arrow.core.raise.ensureNotNull
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.settings.usercountry.models.UserCountryError
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/**
* Get user country code use case
@ -19,7 +21,17 @@ class GetUserCountryUseCase(
private val settingsRepository: SettingsRepository,
) {
suspend operator fun invoke(): Either<UserCountryError, UserCountry> {
operator fun invoke(): Flow<Either<UserCountryError, UserCountry>> {
return settingsRepository.getUserCountryCode().map { userCountryCode ->
either {
ensureNotNull(userCountryCode) { UserCountryError.NotSetup }
userCountryCode
}
}
}
suspend fun invokeSync(): Either<UserCountryError, UserCountry> {
return either {
val userCountryCode = catch(
block = { settingsRepository.getUserCountryCodeSync() },

View file

@ -1,5 +1,6 @@
package com.tangem.features.onramp.tokenlist.entity.transformer
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
@ -19,6 +20,7 @@ internal class UpdateTokenItemsTransformer(
private val statuses: Map<Boolean, List<CryptoCurrencyStatus>>,
private val isBalanceHidden: Boolean,
private val unavailableTokensHeaderReference: TextReference,
private val warning: NotificationUM? = null,
) : TokenListUMTransformer {
override fun transform(prevState: TokenListUM): TokenListUM {
@ -38,7 +40,7 @@ internal class UpdateTokenItemsTransformer(
),
unavailableItems = unavailableItems.addHeader(textReference = unavailableTokensHeaderReference),
isBalanceHidden = isBalanceHidden,
warning = null,
warning = warning,
)
}

View file

@ -1,17 +1,23 @@
package com.tangem.features.onramp.tokenlist.model
import arrow.core.getOrElse
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.resourceReference
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.exchange.RampStateManager
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
@ -25,6 +31,7 @@ import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.isZero
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
@ -43,6 +50,7 @@ internal class OnrampTokenListModel @Inject constructor(
private val getTokenListUseCase: GetTokenListUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val rampStateManager: RampStateManager,
private val getUserCountryUseCase: GetUserCountryUseCase,
) : Model() {
val state: StateFlow<TokenListUM> = tokenListUMController.state
@ -69,7 +77,8 @@ internal class OnrampTokenListModel @Inject constructor(
flow2 = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(),
flow3 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(),
flow4 = searchManager.query,
) { maybeTokenList, appCurrency, isBalanceHidden, query ->
flow5 = hasRestrictionForSellFlow(),
) { maybeTokenList, appCurrency, isBalanceHidden, query, hasRestrictionForSell ->
val currencies = maybeTokenList.getOrElse(
ifLoading = { it ?: TokenList.Empty },
ifError = { TokenList.Empty },
@ -90,16 +99,29 @@ internal class OnrampTokenListModel @Inject constructor(
.let(::resourceReference),
)
} else {
val isInsufficientBalanceForSell = if (params.filterOperation == OnrampOperation.SELL) {
maybeTokenList.isInsufficientBalanceForSell()
} else {
false
}
UpdateTokenItemsTransformer(
appCurrency = appCurrency,
onItemClick = params.onTokenClick,
statuses = filterByQueryTokenList.filterByAvailability(),
statuses = filterByQueryTokenList.let {
if (hasRestrictionForSell || isInsufficientBalanceForSell) {
mapOf(false to it)
} else {
it.filterByAvailability()
}
},
isBalanceHidden = isBalanceHidden,
unavailableTokensHeaderReference = when (params.filterOperation) {
OnrampOperation.BUY -> R.string.tokens_list_unavailable_to_purchase_header
OnrampOperation.SELL -> R.string.tokens_list_unavailable_to_sell_header
OnrampOperation.SWAP -> R.string.tokens_list_unavailable_to_swap_source_header
}.let(::resourceReference),
unavailableTokensHeaderReference = getUnavailableTokensHeaderReference(),
warning = when {
hasRestrictionForSell -> NotificationUM.Warning.SellingRegionalRestriction
isInsufficientBalanceForSell -> NotificationUM.Warning.InsufficientBalanceForSelling
else -> null
},
)
}
}
@ -108,6 +130,32 @@ internal class OnrampTokenListModel @Inject constructor(
.launchIn(modelScope)
}
private fun hasRestrictionForSellFlow(): Flow<Boolean> {
return if (params.filterOperation == OnrampOperation.SELL) {
getUserCountryUseCase().map { maybe ->
maybe.isRight { country -> country is UserCountry.Russia }
}
} else {
flowOf(false)
}
}
private fun Lce<TokenListError, TokenList>.isInsufficientBalanceForSell(): Boolean {
return if (params.filterOperation == OnrampOperation.SELL) {
isContent {
(it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() ?: false
}
} else {
false
}
}
private fun getUnavailableTokensHeaderReference() = when (params.filterOperation) {
OnrampOperation.BUY -> R.string.tokens_list_unavailable_to_purchase_header
OnrampOperation.SELL -> R.string.tokens_list_unavailable_to_sell_header
OnrampOperation.SWAP -> R.string.tokens_list_unavailable_to_swap_source_header
}.let(::resourceReference)
private fun onSearchQueryChange(newQuery: String) {
val searchBar = state.value.searchBarUM
if (searchBar.query == newQuery) return

View file

@ -1,6 +1,7 @@
package com.tangem.features.onramp.tokenlist.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@ -42,14 +43,18 @@ internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) {
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)
AnimatedContent(targetState = state.warning, label = "") { warning ->
when (warning) {
is NotificationUM.Warning.OnrampErrorNotification -> {
Notification(
config = warning.config,
containerColor = TangemTheme.colors.background.primary,
)
}
else -> {
Notification(config = warning.config, containerColor = TangemTheme.colors.button.disabled)
}
}
is NotificationUM.Warning.SwapNoAvailablePair -> {
Notification(config = state.warning.config, containerColor = TangemTheme.colors.button.disabled)
}
else -> Unit
}
}

View file

@ -98,22 +98,6 @@ internal sealed interface WalletAlertState {
override val onConfirmClick: (() -> Unit)? = null
}
data object SellingRegionalRestriction : Basic() {
override val title: TextReference = resourceReference(id = R.string.selling_regional_restriction_alert_title)
override val message: TextReference =
resourceReference(id = R.string.selling_regional_restriction_alert_message)
override val onConfirmClick: (() -> Unit)? = null
}
data object InsufficientBalanceForSelling : Basic() {
override val title: TextReference = resourceReference(id = R.string.selling_insufficient_balance_alert_title)
override val message: TextReference =
resourceReference(id = R.string.selling_insufficient_balance_alert_message)
override val onConfirmClick: (() -> Unit)? = null
}
data object InsufficientTokensCountForSwapping : Basic() {
override val title: TextReference =
resourceReference(id = R.string.action_buttons_swap_no_tokens_added_alert_title)

View file

@ -29,8 +29,6 @@ import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase
@ -46,7 +44,10 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.domain.unwrap
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
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
@ -117,7 +118,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private val shareManager: ShareManager,
private val appRouter: AppRouter,
private val rampStateManager: RampStateManager,
private val getUserCountryUseCase: GetUserCountryUseCase,
private val onrampFeatureToggles: OnrampFeatureToggles,
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
@ -410,33 +410,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
override fun onMultiWalletSellClick(userWalletId: UserWalletId) {
viewModelScope.launch {
val userCountry = getUserCountryUseCase().getOrNull()
if (userCountry is UserCountry.Russia) {
handleError(
alertState = WalletAlertState.SellingRegionalRestriction,
eventCreator = MainScreenAnalyticsEvent::ButtonSell,
)
return@launch
}
val selectedWallet = stateHolder.getSelectedWallet().walletCardState as? WalletCardState.Content
if (selectedWallet?.isZeroBalance == true) {
handleError(
alertState = WalletAlertState.InsufficientBalanceForSelling,
eventCreator = MainScreenAnalyticsEvent::ButtonSell,
)
return@launch
}
onMultiWalletActionClick(
statusFlow = rampStateManager.getSellInitializationStatus(),
route = AppRoute.SellCrypto(userWalletId = userWalletId),
eventCreator = MainScreenAnalyticsEvent::ButtonSell,
)
}
onMultiWalletActionClick(
statusFlow = rampStateManager.getSellInitializationStatus(),
route = AppRoute.SellCrypto(userWalletId = userWalletId),
eventCreator = MainScreenAnalyticsEvent::ButtonSell,
)
}
override fun onMultiWalletSwapClick(userWalletId: UserWalletId) {