Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-02 19:42:44 +03:00
parent 0e61080c25
commit 63f6ae5dfa
16 changed files with 320 additions and 33 deletions

View file

@ -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<AppThemeMode?>
// 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() {

View file

@ -0,0 +1,12 @@
package com.tangem.tap.features.main
internal interface MainIntents {
fun onHiddenBalanceToastAction()
fun onShownBalanceToastAction()
fun onHiddenBalanceNotificationAction(isPermanent: Boolean)
fun onDismissBottomSheet()
}

View file

@ -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<MainScreenState> = MutableStateFlow(getInitialState())
val stateFlow: StateFlow<MainScreenState> = 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,
)
}

View file

@ -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<BalanceHidingSettings> = getBalanceHidingSettingsUseCase()
.shareIn(viewModelScope, SharingStarted.WhileSubscribed())
val state: StateFlow<MainScreenState> = 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()
}
}

View file

@ -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) },
),
)
}
}

View file

@ -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,
)

View file

@ -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<Toast>,
val modalNotification: TangemBottomSheetConfig?,
)

View file

@ -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

View file

@ -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,
)

View file

@ -28,6 +28,10 @@
<string name="app_settings_theme_mode_system">Как в системе</string>
<string name="app_settings_theme_selector_title">Тема</string>
<string name="app_settings_title">Настройки приложения</string>
<string name="balance_hidden_description">Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\"</string>
<string name="balance_hidden_do_not_show_button">Больше не показывать</string>
<string name="balance_hidden_got_it_button">Понятно</string>
<string name="balance_hidden_title">Балансы скрыты</string>
<string name="biometric_lockout_permanent_warning_description">Пожалуйста, отсканируйте карту</string>
<string name="biometric_lockout_warning_description">Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту</string>
<string name="biometric_lockout_warning_title">Слишком много попыток</string>
@ -452,6 +456,9 @@
<string name="swapping_token_list_title">Выберите токен</string>
<string name="swapping_token_list_your_tokens">Ваши токены</string>
<string name="swapping_token_not_available">не доступен</string>
<string name="toast_balances_hidden">Балансы скрыты</string>
<string name="toast_balances_shown">Балансы показаны</string>
<string name="toast_undo">Отменить</string>
<plurals name="token_count">
<item quantity="one">%d токен</item>
<item quantity="few">%d токена</item>

View file

@ -28,6 +28,10 @@
<string name="app_settings_theme_mode_system">System default</string>
<string name="app_settings_theme_selector_title">Theme</string>
<string name="app_settings_title">App Settings</string>
<string name="balance_hidden_description">To hide or show your balances, simply flip your device screen down, or switch it off in Settings</string>
<string name="balance_hidden_do_not_show_button">Don\'t show again</string>
<string name="balance_hidden_got_it_button">Got it</string>
<string name="balance_hidden_title">Balances are hidden</string>
<string name="biometric_lockout_permanent_warning_description">Please scan the card</string>
<string name="biometric_lockout_warning_description">Please try again in 30 seconds or scan the card</string>
<string name="biometric_lockout_warning_title">Too many attempts</string>
@ -453,6 +457,9 @@
<string name="swapping_token_list_title">Choose token</string>
<string name="swapping_token_list_your_tokens">Your tokens</string>
<string name="swapping_token_not_available">not available</string>
<string name="toast_balances_hidden">Balances hidden</string>
<string name="toast_balances_shown">Balances shown</string>
<string name="toast_undo">Undo</string>
<plurals name="token_count">
<item quantity="one">%d token</item>
<item quantity="other">%d tokens</item>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#1E1E1E"
android:pathData="M2,5.27L3.28,4L20,20.72L18.73,22L15.65,18.92C14.5,19.3 13.28,19.5 12,19.5C7,19.5 2.73,16.39 1,12C1.69,10.24 2.79,8.69 4.19,7.46L2,5.27ZM12,9C12.796,9 13.559,9.316 14.121,9.879C14.684,10.441 15,11.204 15,12C15,12.35 14.94,12.69 14.83,13L11,9.17C11.31,9.06 11.65,9 12,9ZM12,4.5C17,4.5 21.27,7.61 23,12C22.18,14.08 20.79,15.88 19,17.19L17.58,15.76C18.94,14.82 20.06,13.54 20.82,12C19.17,8.64 15.76,6.5 12,6.5C10.91,6.5 9.84,6.68 8.84,7L7.3,5.47C8.74,4.85 10.33,4.5 12,4.5ZM3.18,12C4.83,15.36 8.24,17.5 12,17.5C12.69,17.5 13.37,17.43 14,17.29L11.72,15C10.29,14.85 9.15,13.71 9,12.28L5.6,8.87C4.61,9.72 3.78,10.78 3.18,12Z" />
</vector>

View file

@ -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() {

View file

@ -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) {

View file

@ -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()
}

View file

@ -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<UserWallet>) {