diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index b5a7b75c92..6faa43d670 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -9,6 +9,7 @@ import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.view.View +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode @@ -53,6 +54,7 @@ import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandle import com.tangem.tap.features.intentHandler.handlers.BuyCurrencyIntentHandler import com.tangem.tap.features.intentHandler.handlers.SellCurrencyIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler +import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.tap.features.welcome.ui.WelcomeFragment @@ -126,6 +128,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var sendRouter: SendRouter + internal val viewModel: MainViewModel by viewModels() + private lateinit var appThemeModeFlow: SharedFlow // TODO: fixme: inject through DI @@ -152,6 +156,16 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac checkGooglePayAvailability() checkForNotificationPermission() + observeStateUpdates() + } + + private fun observeStateUpdates() { + viewModel.state + .flowWithLifecycle(lifecycle) + .onEach { + // TODO: Show toast + } + .launchIn(lifecycleScope) } private fun installActivityDependencies() { diff --git a/app/src/main/java/com/tangem/tap/features/main/MainIntents.kt b/app/src/main/java/com/tangem/tap/features/main/MainIntents.kt new file mode 100644 index 0000000000..bfbfb42379 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/MainIntents.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.features.main + +internal interface MainIntents { + + fun onHiddenBalanceToastAction() + + fun onShownBalanceToastAction() + + fun onHiddenBalanceNotificationAction(isPermanent: Boolean) + + fun onDismissBottomSheet() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainScreenStateHolder.kt b/app/src/main/java/com/tangem/tap/features/main/MainScreenStateHolder.kt new file mode 100644 index 0000000000..9bbeded200 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/MainScreenStateHolder.kt @@ -0,0 +1,62 @@ +package com.tangem.tap.features.main + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.tap.features.main.model.MainScreenState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +internal class MainScreenStateHolder(private val intents: MainIntents) { + + private val notificationsFactory = NotificationsFactory(intents) + + private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) + + val stateFlow: StateFlow = stateFlowInternal + + fun updateWithHiddenBalancesToast(isBalanceHidden: Boolean) { + stateFlowInternal.update { state -> + state.copy( + toast = triggeredEvent( + data = if (isBalanceHidden) { + notificationsFactory.createBalancesAreHiddenToast() + } else { + notificationsFactory.createBalancesAreShownToast() + }, + onConsume = ::consumeToastEvent, + ), + ) + } + } + + fun updateWithHiddenBalancesNotification() { + stateFlowInternal.update { state -> + state.copy( + modalNotification = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = intents::onDismissBottomSheet, + content = notificationsFactory.createBalancesAreHiddenModalNotification(), + ), + ) + } + } + + fun updateWithoutModalNotification() { + stateFlowInternal.update { state -> + state.copy(modalNotification = state.modalNotification?.copy(isShow = false)) + } + } + + private fun consumeToastEvent() { + stateFlowInternal.update { state -> + state.copy(toast = consumedEvent()) + } + } + + private fun getInitialState() = MainScreenState( + toast = consumedEvent(), + modalNotification = null, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt new file mode 100644 index 0000000000..cf46a567f9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -0,0 +1,123 @@ +package com.tangem.tap.features.main + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.domain.balancehiding.BalanceHidingSettings +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.balancehiding.ListenToFlipsUseCase +import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase +import com.tangem.tap.features.main.model.MainScreenState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class MainViewModel @Inject constructor( + private val updateBalanceHidingSettingsUseCase: UpdateBalanceHidingSettingsUseCase, + private val listenToFlipsUseCase: ListenToFlipsUseCase, + getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, +) : ViewModel(), MainIntents { + + private val stateHolder = MainScreenStateHolder( + intents = this, + ) + + private val balanceHidingSettingsFlow: SharedFlow = getBalanceHidingSettingsUseCase() + .shareIn(viewModelScope, SharingStarted.WhileSubscribed()) + + val state: StateFlow = stateHolder.stateFlow + + init { + observeFlips() + displayBalancesHidingStatusToast() + displayHiddenBalancesModalNotification() + } + + private fun observeFlips() { + listenToFlipsUseCase().launchIn(viewModelScope) + } + + private fun displayHiddenBalancesModalNotification() { + balanceHidingSettingsFlow + .drop(count = 1) // Skip initial emit + .distinctUntilChanged() + .filter { + it.isBalanceHidingNotificationEnabled && it.isBalanceHidden + } + .onEach { + if (state.value.modalNotification?.isShow != true) { + stateHolder.updateWithHiddenBalancesNotification() + // TODO: reduxNavController.navigate(NavigationAction.NavigateTo(AppScreen.ModalNotification)) + } + } + .launchIn(viewModelScope) + } + + private fun displayBalancesHidingStatusToast() { + var previousSettings: BalanceHidingSettings? = null + + balanceHidingSettingsFlow + .onEach { settings -> + if (previousSettings == null) { + previousSettings = settings + return@onEach + } + + /* + * Skip if the feature to hide balances has just been + * enabled or disabled in the settings, or if the hide balances status has not been changed + * */ + if (settings.isHidingEnabledInSettings != previousSettings?.isHidingEnabledInSettings || + settings.isBalanceHidden == previousSettings?.isBalanceHidden + ) { + previousSettings = settings + return@onEach + } + + displayBalancesHiddenStatusToast(settings) + + previousSettings = settings + } + .launchIn(viewModelScope) + } + + private fun displayBalancesHiddenStatusToast(settings: BalanceHidingSettings) { + // If modal notification is enabled and balances are hidden, the toast will not show + if (!settings.isBalanceHidingNotificationEnabled || !settings.isBalanceHidden) { + stateHolder.updateWithHiddenBalancesToast(settings.isBalanceHidden) + } + } + + override fun onHiddenBalanceToastAction() { + viewModelScope.launch { + updateBalanceHidingSettingsUseCase.invoke { + copy(isBalanceHidden = false) + } + } + } + + override fun onShownBalanceToastAction() { + viewModelScope.launch { + updateBalanceHidingSettingsUseCase.invoke { + copy(isBalanceHidden = true) + } + } + } + + override fun onHiddenBalanceNotificationAction(isPermanent: Boolean) { + onDismissBottomSheet() + + if (isPermanent) { + viewModelScope.launch { + updateBalanceHidingSettingsUseCase.invoke { + copy(isBalanceHidingNotificationEnabled = false) + } + } + } + } + + override fun onDismissBottomSheet() { + stateHolder.updateWithoutModalNotification() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/NotificationsFactory.kt b/app/src/main/java/com/tangem/tap/features/main/NotificationsFactory.kt new file mode 100644 index 0000000000..cc2dcc6c40 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/NotificationsFactory.kt @@ -0,0 +1,48 @@ +package com.tangem.tap.features.main + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.tap.features.main.model.ActionConfig +import com.tangem.tap.features.main.model.ModalNotification +import com.tangem.tap.features.main.model.Toast +import com.tangem.wallet.R + +internal class NotificationsFactory( + private val intents: MainIntents, +) { + + fun createBalancesAreHiddenToast(): Toast { + return Toast( + message = resourceReference(R.string.toast_balances_hidden), + action = ActionConfig( + text = resourceReference(R.string.toast_undo), + onClick = intents::onHiddenBalanceToastAction, + ), + ) + } + + fun createBalancesAreShownToast(): Toast { + return Toast( + message = resourceReference(R.string.toast_balances_shown), + action = ActionConfig( + text = resourceReference(R.string.toast_undo), + onClick = intents::onShownBalanceToastAction, + ), + ) + } + + fun createBalancesAreHiddenModalNotification(): ModalNotification { + return ModalNotification( + iconResId = R.drawable.ic_eye_off_outline_24, + title = resourceReference(R.string.balance_hidden_title), + message = resourceReference(R.string.balance_hidden_description), + primaryAction = ActionConfig( + text = resourceReference(R.string.balance_hidden_got_it_button), + onClick = { intents.onHiddenBalanceNotificationAction(isPermanent = false) }, + ), + secondaryAction = ActionConfig( + text = resourceReference(R.string.balance_hidden_do_not_show_button), + onClick = { intents.onHiddenBalanceNotificationAction(isPermanent = true) }, + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/model/ActionConfig.kt b/app/src/main/java/com/tangem/tap/features/main/model/ActionConfig.kt new file mode 100644 index 0000000000..6549c35d88 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/model/ActionConfig.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.features.main.model + +import com.tangem.core.ui.extensions.TextReference + +internal data class ActionConfig( + val text: TextReference, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/model/MainScreenState.kt b/app/src/main/java/com/tangem/tap/features/main/model/MainScreenState.kt new file mode 100644 index 0000000000..7a787bbc7b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/model/MainScreenState.kt @@ -0,0 +1,9 @@ +package com.tangem.tap.features.main.model + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.event.StateEvent + +internal data class MainScreenState( + val toast: StateEvent, + val modalNotification: TangemBottomSheetConfig?, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/model/ModalNotification.kt b/app/src/main/java/com/tangem/tap/features/main/model/ModalNotification.kt new file mode 100644 index 0000000000..eaf3d37833 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/model/ModalNotification.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.features.main.model + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference + +internal data class ModalNotification( + @DrawableRes val iconResId: Int, + val title: TextReference, + val message: TextReference, + val primaryAction: ActionConfig, + val secondaryAction: ActionConfig?, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/model/Toast.kt b/app/src/main/java/com/tangem/tap/features/main/model/Toast.kt new file mode 100644 index 0000000000..9e959eb892 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/model/Toast.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.features.main.model + +import com.tangem.core.ui.extensions.TextReference + +internal data class Toast( + val message: TextReference, + val action: ActionConfig, +) \ No newline at end of file diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 0d96639f41..8532b9050c 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -28,6 +28,10 @@ Как в системе Тема Настройки приложения + Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\" + Больше не показывать + Понятно + Балансы скрыты Пожалуйста, отсканируйте карту Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту Слишком много попыток @@ -452,6 +456,9 @@ Выберите токен Ваши токены не доступен + Балансы скрыты + Балансы показаны + Отменить %d токен %d токена diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 5c971d0845..ab48be1b10 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -28,6 +28,10 @@ System default Theme App Settings + To hide or show your balances, simply flip your device screen down, or switch it off in Settings + Don\'t show again + Got it + Balances are hidden Please scan the card Please try again in 30 seconds or scan the card Too many attempts @@ -453,6 +457,9 @@ Choose token Your tokens not available + Balances hidden + Balances shown + Undo %d token %d tokens diff --git a/core/ui/src/main/res/drawable/ic_eye_off_outline_24.xml b/core/ui/src/main/res/drawable/ic_eye_off_outline_24.xml new file mode 100644 index 0000000000..c7958120c8 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_eye_off_outline_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 149ea1f2c1..c5e7f74bc7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -8,7 +8,6 @@ import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.InputNumberFormatter import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.tokens.model.Network import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor @@ -29,7 +28,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.runCatching import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -50,7 +48,6 @@ internal class SwapViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val listenToFlipsUseCase: ListenToFlipsUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { @@ -105,12 +102,6 @@ internal class SwapViewModel @Inject constructor( } } .launchIn(viewModelScope) - - viewModelScope.launch { - listenToFlipsUseCase() - .flowWithLifecycle(owner.lifecycle) - .collect() - } } override fun onCleared() { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 53bbdcbb29..e7addd38b0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -16,7 +16,6 @@ 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.balancehiding.ListenToFlipsUseCase import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder @@ -66,7 +65,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val removeCurrencyUseCase: RemoveCurrencyUseCase, private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val listenToFlipsUseCase: ListenToFlipsUseCase, private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val walletManagersFacade: WalletManagersFacade, @@ -125,12 +123,6 @@ internal class TokenDetailsViewModel @Inject constructor( ) } .launchIn(viewModelScope) - - viewModelScope.launch { - listenToFlipsUseCase() - .flowWithLifecycle(owner.lifecycle) - .collect() - } } private suspend fun updateButtons(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index e3c4744eb5..89a9cce078 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -8,7 +8,6 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase @@ -39,7 +38,6 @@ internal class OrganizeTokensViewModel @Inject constructor( private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val listenToFlipsUseCase: ListenToFlipsUseCase, private val analyticsEventsHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, savedStateHandle: SavedStateHandle, @@ -82,12 +80,6 @@ internal class OrganizeTokensViewModel @Inject constructor( } .launchIn(viewModelScope) - viewModelScope.launch { - listenToFlipsUseCase() - .flowWithLifecycle(owner.lifecycle) - .collect() - } - bootstrapTokenList() bootstrapDragAndDropUpdates() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index baf99e30bf..d943c0f76a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -28,7 +28,6 @@ import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.common.CardTypesResolver @@ -116,7 +115,6 @@ internal class WalletViewModel @Inject constructor( private val canUseBiometryUseCase: CanUseBiometryUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val listenToFlipsUseCase: ListenToFlipsUseCase, private val removeCurrencyUseCase: RemoveCurrencyUseCase, private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, private val walletManagersFacade: WalletManagersFacade, @@ -220,12 +218,6 @@ internal class WalletViewModel @Inject constructor( uiState = stateFactory.getHiddenBalanceState(isBalanceHidden = it.isBalanceHidden) } .launchIn(viewModelScope) - - viewModelScope.launch { - listenToFlipsUseCase() - .flowWithLifecycle(owner.lifecycle) - .collect() - } } private fun updateWallets(sourceList: List) {