diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 8859c88746..796b99ce27 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -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) { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 392f1aacd9..6c9ed1d2e8 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -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 { 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 9f5bf56e7d..cb008ba04a 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 @@ -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( diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 3c6c740e2e..789406f504 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -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 = userCountryFlow + override suspend fun fetchUserCountryCode() { Timber.i("Start fetching user country code") diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 22f8fec52b..8670f0ce4f 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -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 + suspend fun fetchUserCountryCode() suspend fun setGoogleServicesAvailability(value: Boolean) diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/usercountry/GetUserCountryUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/usercountry/GetUserCountryUseCase.kt index 01768f5a6c..cabd6677a9 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/usercountry/GetUserCountryUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/usercountry/GetUserCountryUseCase.kt @@ -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 { + operator fun invoke(): Flow> { + return settingsRepository.getUserCountryCode().map { userCountryCode -> + either { + ensureNotNull(userCountryCode) { UserCountryError.NotSetup } + + userCountryCode + } + } + } + + suspend fun invokeSync(): Either { return either { val userCountryCode = catch( block = { settingsRepository.getUserCountryCodeSync() }, 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 2913ce4223..2deda8e910 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,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>, 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, ) } 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 6568ab9e04..84890669cc 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 @@ -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 = 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 { + return if (params.filterOperation == OnrampOperation.SELL) { + getUserCountryUseCase().map { maybe -> + maybe.isRight { country -> country is UserCountry.Russia } + } + } else { + flowOf(false) + } + } + + private fun Lce.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 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 069b27e874..679abe3866 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 @@ -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 } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt index 63855cabc5..5c8ab22a90 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt @@ -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) 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 43630b4d4b..db4d42f963 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 @@ -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) {