Updated on 2026-08-14

This commit is contained in:
Tangem 2024-01-22 13:40:55 +03:00
parent 9b2becff1d
commit 8df9a2f8e2
8 changed files with 170 additions and 72 deletions

View file

@ -1,21 +1,22 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
internal class AppSettingsFragment : ComposeFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
@ -23,43 +24,20 @@ internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsS
@Inject
lateinit var appCurrencyRepository: AppCurrencyRepository
private val viewModel by lazy(mode = LazyThreadSafetyMode.NONE) {
AppSettingsViewModel(store, appCurrencyRepository)
}
@Composable
override fun ScreenContent(modifier: Modifier) {
val viewModel = hiltViewModel<AppSettingsViewModel>().apply {
LocalLifecycleOwner.current.lifecycle.addObserver(observer = this)
}
val state by viewModel.uiState.collectAsStateWithLifecycle()
AppSettingsScreen(
modifier = modifier,
state = viewModel.uiState,
state = state,
onBackClick = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatch(NavigationAction.PopBackTo())
},
)
}
override fun onResume() {
super.onResume()
viewModel.checkBiometricsStatus(lifecycleScope)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
viewModel.updateState(state)
}
}

View file

@ -10,7 +10,7 @@ internal class AppSettingsItemsFactory {
fun createEnrollBiometricsCard(onClick: () -> Unit): Item.Card {
return Item.Card(
id = "enroll_biometrics_card",
id = ID_ENROLL_BIOMETRICS_CARD,
title = resourceReference(R.string.app_settings_enable_biometrics_title),
description = resourceReference(R.string.app_settings_enable_biometrics_description),
iconResId = R.drawable.ic_alert_circle_24,
@ -24,7 +24,7 @@ internal class AppSettingsItemsFactory {
onCheckedChange: (Boolean) -> Unit,
): Item.Switch {
return Item.Switch(
id = "save_wallets_switch",
id = ID_SAVE_WALLETS_SWITCH,
title = resourceReference(R.string.app_settings_saved_wallet),
description = resourceReference(R.string.app_settings_saved_wallet_footer),
isEnabled = isEnabled,
@ -39,7 +39,7 @@ internal class AppSettingsItemsFactory {
onCheckedChange: (Boolean) -> Unit,
): Item.Switch {
return Item.Switch(
id = "save_access_codes_switch",
id = ID_SAVE_ACCESS_CODES_SWITCH,
title = resourceReference(R.string.app_settings_saved_access_codes),
description = resourceReference(R.string.app_settings_saved_access_codes_footer),
isEnabled = isEnabled,
@ -54,7 +54,7 @@ internal class AppSettingsItemsFactory {
onCheckedChange: (Boolean) -> Unit,
): Item.Switch {
return Item.Switch(
id = "flip_to_hide_balance_switch",
id = ID_FLIP_TO_HIDE_BALANCE_SWITCH,
title = resourceReference(R.string.details_row_title_flip_to_hide),
description = resourceReference(R.string.details_row_description_flip_to_hide),
isEnabled = isEnabled,
@ -65,7 +65,7 @@ internal class AppSettingsItemsFactory {
fun createSelectAppCurrencyButton(currentAppCurrencyName: String, onClick: () -> Unit): Item.Button {
return Item.Button(
id = "select_app_currency_button",
id = ID_SELECT_APP_CURRENCY_BUTTON,
title = resourceReference(R.string.details_row_title_currency),
description = stringReference(currentAppCurrencyName),
isEnabled = true,
@ -75,7 +75,7 @@ internal class AppSettingsItemsFactory {
fun createSelectThemeModeButton(currentThemeMode: AppThemeMode, onClick: () -> Unit): Item.Button {
return Item.Button(
id = "select_theme_mode_button",
id = ID_SELECT_THEME_MODE_BUTTON,
title = resourceReference(R.string.app_settings_theme_selector_title),
description = resourceReference(
id = when (currentThemeMode) {
@ -88,4 +88,13 @@ internal class AppSettingsItemsFactory {
onClick = onClick,
)
}
companion object {
const val ID_ENROLL_BIOMETRICS_CARD = "enroll_biometrics_card"
const val ID_SAVE_WALLETS_SWITCH = "save_wallets_switch"
const val ID_SAVE_ACCESS_CODES_SWITCH = "save_access_codes_switch"
const val ID_FLIP_TO_HIDE_BALANCE_SWITCH = "flip_to_hide_balance_switch"
const val ID_SELECT_APP_CURRENCY_BUTTON = "select_app_currency_button"
const val ID_SELECT_THEME_MODE_BUTTON = "select_theme_mode_button"
}
}

View file

@ -1,10 +1,10 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.core.analytics.Analytics
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
@ -13,53 +13,78 @@ import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.AppSetting
import com.tangem.tap.features.details.redux.AppSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.ui.appsettings.analytics.AppSettingsItemsAnalyticsSender
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.rekotlin.Store
import kotlinx.coroutines.flow.*
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
internal class AppSettingsViewModel(
private val store: Store<AppState>,
@HiltViewModel
internal class AppSettingsViewModel @Inject constructor(
private val appCurrencyRepository: AppCurrencyRepository,
) {
private val analyticsEventHandler: AnalyticsEventHandler,
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
) : ViewModel(),
StoreSubscriber<DetailsState>,
DefaultLifecycleObserver {
private val itemsFactory = AppSettingsItemsFactory()
private val dialogsFactory = AppSettingsDialogsFactory()
private val appCurrencyUpdatesJobHolder = JobHolder()
var uiState: AppSettingsScreenState by mutableStateOf(AppSettingsScreenState.Loading)
private set
private val _uiState: MutableStateFlow<AppSettingsScreenState> = MutableStateFlow(
value = AppSettingsScreenState.Loading,
)
val uiState: StateFlow<AppSettingsScreenState> = _uiState
init {
bootstrapAppCurrencyUpdates()
subscribeToStoreChanges()
sendItemsAnalytics()
}
fun updateState(state: DetailsState) {
uiState = AppSettingsScreenState.Content(
items = buildItems(state.appSettingsState),
dialog = (uiState as? AppSettingsScreenState.Content)?.dialog,
)
override fun newState(state: DetailsState) {
val items = buildItems(state.appSettingsState)
_uiState.update { prevState ->
when (prevState) {
is AppSettingsScreenState.Content -> prevState.copy(
items = items,
)
is AppSettingsScreenState.Loading -> AppSettingsScreenState.Content(
items = items,
dialog = null,
)
}
}
}
fun checkBiometricsStatus(lifecycleScope: LifecycleCoroutineScope) {
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(lifecycleScope))
override fun onResume(owner: LifecycleOwner) {
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(owner.lifecycleScope))
}
override fun onCleared() {
store.unsubscribe(subscriber = this)
}
private fun buildItems(state: AppSettingsState): ImmutableList<AppSettingsScreenState.Item> {
val items = buildList {
if (state.needEnrollBiometrics) {
Analytics.send(Settings.AppSettings.EnableBiometrics)
itemsFactory.createEnrollBiometricsCard(onClick = ::enrollBiometrics).let(::add)
itemsFactory.createEnrollBiometricsCard(
onClick = ::enrollBiometrics,
).let(::add)
}
itemsFactory.createSelectAppCurrencyButton(
@ -89,9 +114,10 @@ internal class AppSettingsViewModel(
onCheckedChange = ::onFlipToHideBalanceToggled,
).let(::add)
itemsFactory.createSelectThemeModeButton(state.selectedThemeMode) {
showThemeModeSelector(state.selectedThemeMode)
}.let(::add)
itemsFactory.createSelectThemeModeButton(
currentThemeMode = state.selectedThemeMode,
onClick = { showThemeModeSelector(state.selectedThemeMode) },
).let(::add)
}
return items.toImmutableList()
@ -111,7 +137,7 @@ internal class AppSettingsViewModel(
dialog = dialogsFactory.createThemeModeSelectorDialog(
selectedModeIndex = selectedMode.ordinal,
onSelect = { mode ->
Analytics.send(
analyticsEventHandler.send(
event = Settings.AppSettings.ThemeSwitched(
theme = AnalyticsParam.AppTheme.fromAppThemeMode(mode),
),
@ -167,7 +193,7 @@ internal class AppSettingsViewModel(
private fun onFlipToHideBalanceToggled(enable: Boolean) {
val param = AnalyticsParam.OnOffState(enable)
Analytics.send(Settings.AppSettings.HideBalanceChanged(param))
analyticsEventHandler.send(Settings.AppSettings.HideBalanceChanged(param))
store.dispatch(DetailsAction.AppSettings.ChangeBalanceHiding(hideBalance = enable))
}
@ -188,10 +214,28 @@ internal class AppSettingsViewModel(
.saveIn(appCurrencyUpdatesJobHolder)
}
private fun subscribeToStoreChanges() {
store.subscribe(subscriber = this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
private fun sendItemsAnalytics() {
uiState
.filterIsInstance<AppSettingsScreenState.Content>()
.distinctUntilChangedBy(AppSettingsScreenState.Content::items)
.onEach { appSettingsItemsAnalyticsSender.send(it.items) }
.launchIn(scope)
}
private fun updateContentState(block: AppSettingsScreenState.Content.() -> AppSettingsScreenState.Content) {
uiState = when (val state = uiState) {
is AppSettingsScreenState.Content -> block(state)
is AppSettingsScreenState.Loading -> state
_uiState.update { prevState ->
when (prevState) {
is AppSettingsScreenState.Content -> block(prevState)
is AppSettingsScreenState.Loading -> prevState
}
}
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.tap.features.details.ui.appsettings.analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState
import dagger.hilt.android.scopes.ViewModelScoped
import javax.inject.Inject
@ViewModelScoped
internal class AppSettingsItemsAnalyticsSender @Inject constructor(
private val analyticsHandler: AnalyticsEventHandler,
) {
fun send(items: List<AppSettingsScreenState.Item>) {
val events = getEvents(items)
events.forEach { analyticsHandler.send(it) }
}
private fun getEvents(items: List<AppSettingsScreenState.Item>): Set<AnalyticsEvent> {
return items.mapNotNullTo(mutableSetOf(), ::getEvent)
}
private fun getEvent(item: AppSettingsScreenState.Item): AnalyticsEvent? {
return when (item.id) {
AppSettingsItemsFactory.ID_ENROLL_BIOMETRICS_CARD -> Settings.AppSettings.EnableBiometrics
else -> null
}
}
}