Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-31 14:31:37 +04:00
parent 11d218a155
commit 5f2acac137
8 changed files with 214 additions and 493 deletions

View file

@ -1,7 +1,6 @@
package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.proxy.redux.DaggerGraphReducer
import org.rekotlin.Action
@ -10,7 +9,6 @@ fun appReducer(action: Action, state: AppState): AppState {
return AppState(
globalState = globalReducer(action, state),
detailsState = DetailsReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
)
}

View file

@ -1,8 +1,6 @@
package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphState
import org.rekotlin.Middleware
@ -10,7 +8,6 @@ import org.rekotlin.StateType
data class AppState(
val globalState: GlobalState = GlobalState(),
val detailsState: DetailsState = DetailsState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
@ -18,7 +15,6 @@ data class AppState(
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
logMiddleware,
DetailsMiddleware().detailsMiddleware,
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware,
DaggerGraphMiddleware.daggerGraphMiddleware,

View file

@ -1,47 +0,0 @@
package com.tangem.tap.features.details.redux
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
@Suppress("BooleanPropertyNaming")
sealed class DetailsAction : Action {
sealed class AppSettings : DetailsAction() {
data class SwitchPrivacySetting(
val enable: Boolean,
val setting: AppSetting,
) : AppSettings() {
data object Success : AppSettings()
data class Failure(
val prevState: Boolean,
val setting: AppSetting,
) : AppSettings()
}
data class CheckBiometricsStatus(
val coroutineScope: CoroutineScope,
) : AppSettings()
data object EnrollBiometrics : AppSettings()
data class BiometricsStatusChanged(
val isEnrollBiometricsNeeded: Boolean,
) : AppSettings()
data class ChangeAppThemeMode(
val appThemeMode: AppThemeMode,
) : AppSettings()
data class ChangeBalanceHiding(
val shouldHideBalance: Boolean,
) : AppSettings()
data class ChangeAppCurrency(
val currency: AppCurrency,
) : AppSettings()
data class Prepare(val state: AppSettingsState) : AppSettings()
}
}

View file

@ -1,231 +0,0 @@
package com.tangem.tap.features.details.redux
import com.tangem.common.CompletionResult
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
@Suppress("MemberNameEqualsClassName")
class DetailsMiddleware {
private val appSettingsMiddleware = AppSettingsMiddleware()
val detailsMiddleware: Middleware<AppState> = { _, stateProvider ->
{ next ->
{ action ->
if (!DemoHelper.tryHandle(stateProvider)) {
val detailsState = stateProvider()?.detailsState
if (detailsState != null) {
handleAction(action)
}
}
next(action)
}
}
}
private fun handleAction(action: Action) {
when (action) {
is DetailsAction.AppSettings -> appSettingsMiddleware.handle(action)
}
}
class AppSettingsMiddleware {
private val checkBiometricsStatusJobHolder = JobHolder()
fun handle(action: DetailsAction.AppSettings) {
when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
when (action.setting) {
AppSetting.RequireAccessCode -> toggleRequireAccessCode(enable = action.enable)
AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication(enable = action.enable)
}
}
is DetailsAction.AppSettings.CheckBiometricsStatus -> {
observeBiometricsStatusChanges(action.coroutineScope)
}
is DetailsAction.AppSettings.EnrollBiometrics -> {
enrollBiometrics()
}
is DetailsAction.AppSettings.ChangeAppThemeMode -> {
changeAppThemeMode(action.appThemeMode)
}
is DetailsAction.AppSettings.ChangeBalanceHiding -> {
changeBalanceHiding(action.shouldHideBalance)
}
is DetailsAction.AppSettings.ChangeAppCurrency,
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
is DetailsAction.AppSettings.BiometricsStatusChanged,
is DetailsAction.AppSettings.Prepare,
-> Unit
}
}
private fun toggleBiometricsAuthentication(enable: Boolean) {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
// Nothing to change
if (walletsRepository.useBiometricAuthentication() == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
if (enable) {
setBiometricLockForAllWallets()
} else {
// Remove all biometric-related data
removeAllBiometricData()
walletsRepository.setRequireAccessCode(value = true)
}
walletsRepository.setUseBiometricAuthentication(value = enable)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private fun toggleRequireAccessCode(enable: Boolean) {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
// Nothing to change
if (walletsRepository.requireAccessCode() == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
if (enable) {
// Remove all saved access codes
removeAllBiometricSingData()
}
walletsRepository.setRequireAccessCode(value = enable)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private suspend fun setBiometricLockForAllWallets() {
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val userWallets = userWalletsListRepository.userWalletsSync()
userWallets.forEach { wallet ->
userWalletsListRepository.setLock(
userWalletId = wallet.walletId,
lockMethod = LockMethod.Biometric,
changeUnsecured = false,
)
}
}
private suspend fun removeAllBiometricData() {
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
userWalletsListRepository.userWalletsSync().forEach {
userWalletsListRepository.removeBiometricLock(it.walletId)
}
removeAllBiometricSingData()
}
private suspend fun removeAllBiometricSingData() {
deleteSavedAccessCodes()
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val tangemHotSdk = store.inject(DaggerGraphState::tangemHotSdk)
userWalletsListRepository.userWalletsSync().forEach { wallet ->
if (wallet is UserWallet.Hot) {
userWalletsListRepository.saveWithoutLock(
userWallet = wallet.copy(
hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(wallet.hotWalletId),
),
)
}
}
}
private fun observeBiometricsStatusChanges(scope: CoroutineScope) {
val needEnrollBiometricsFlow = flow {
do {
val isEnrollBiometricsNeeded = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull()
if (isEnrollBiometricsNeeded != null) {
emit(isEnrollBiometricsNeeded)
}
delay(timeMillis = 200)
} while (true)
}
needEnrollBiometricsFlow
.distinctUntilChanged()
.onEach { needEnrollBiometrics ->
store.dispatchWithMain(DetailsAction.AppSettings.BiometricsStatusChanged(needEnrollBiometrics))
}
.launchIn(scope)
.saveIn(checkBiometricsStatusJobHolder)
}
private fun enrollBiometrics() {
Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication())
store.inject(DaggerGraphState::settingsManager).openBiometricSettings()
}
private fun changeAppThemeMode(appThemeMode: AppThemeMode) {
val repository = store.inject(DaggerGraphState::appThemeModeRepository)
scope.launch {
repository.changeAppThemeMode(appThemeMode)
}
}
private fun changeBalanceHiding(hideBalance: Boolean) {
val repository = store.inject(DaggerGraphState::balanceHidingRepository)
scope.launch {
val newState = repository.getBalanceHidingSettings().copy(
isHidingEnabledInSettings = hideBalance,
isBalanceHidden = false,
)
repository.storeBalanceHidingSettings(newState)
}
}
private suspend fun deleteSavedAccessCodes(): CompletionResult<Unit> {
return tangemSdkManager.clearSavedUserCodes()
.doOnSuccess {
Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.Off))
store.inject(DaggerGraphState::settingsRepository).setShouldSaveAccessCodes(value = false)
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = false,
)
}
.doOnFailure { error ->
TangemLogger.e("Unable to delete saved access codes", error)
}
}
}
}

View file

@ -1,89 +0,0 @@
package com.tangem.tap.features.details.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
object DetailsReducer {
fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state)
}
@Suppress("CyclomaticComplexMethod")
private fun internalReduce(action: Action, state: AppState): DetailsState {
if (action !is DetailsAction) return state.detailsState
val detailsState = state.detailsState
return when (action) {
is DetailsAction.AppSettings -> {
handlePrivacyAction(action, detailsState)
}
}
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState {
return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy(
appSettingsState = when (action.setting) {
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
isInProgress = true,
requireAccessCode = action.enable,
)
AppSetting.BiometricAuthentication -> state.appSettingsState.copy(
isInProgress = true,
useBiometricAuthentication = action.enable,
)
},
)
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> state.copy(
appSettingsState = state.appSettingsState.copy(
isInProgress = false,
),
)
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure -> state.copy(
appSettingsState = when (action.setting) {
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
isInProgress = false,
requireAccessCode = action.prevState,
)
AppSetting.BiometricAuthentication -> state.appSettingsState.copy(
isInProgress = false,
needEnrollBiometrics = action.prevState,
)
},
)
is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy(
appSettingsState = state.appSettingsState.copy(
needEnrollBiometrics = action.isEnrollBiometricsNeeded,
),
)
is DetailsAction.AppSettings.ChangeAppThemeMode -> state.copy(
appSettingsState = state.appSettingsState.copy(
selectedThemeMode = action.appThemeMode,
),
)
is DetailsAction.AppSettings.ChangeAppCurrency -> state.copy(
appSettingsState = state.appSettingsState.copy(
selectedAppCurrency = action.currency,
),
)
is DetailsAction.AppSettings.ChangeBalanceHiding -> state.copy(
appSettingsState = state.appSettingsState.copy(
isHidingEnabled = action.shouldHideBalance,
),
)
// state should be copied to avoid concurrent modifications from different sources
is DetailsAction.AppSettings.Prepare -> state.copy(
appSettingsState = state.appSettingsState.copy(
isHidingEnabled = action.state.isHidingEnabled,
selectedAppCurrency = action.state.selectedAppCurrency,
selectedThemeMode = action.state.selectedThemeMode,
useBiometricAuthentication = action.state.useBiometricAuthentication,
requireAccessCode = action.state.requireAccessCode,
hasSecuredWallets = action.state.hasSecuredWallets,
needEnrollBiometrics = action.state.needEnrollBiometrics,
),
)
is DetailsAction.AppSettings.EnrollBiometrics,
is DetailsAction.AppSettings.CheckBiometricsStatus,
-> state
}
}

View file

@ -1,27 +0,0 @@
package com.tangem.tap.features.details.redux
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import org.rekotlin.StateType
data class DetailsState(
val appSettingsState: AppSettingsState = AppSettingsState(),
) : StateType
@Suppress("BooleanPropertyNaming")
data class AppSettingsState(
val requireAccessCode: Boolean = false,
val useBiometricAuthentication: Boolean = false,
val needEnrollBiometrics: Boolean = false,
val hasSecuredWallets: Boolean = false,
val isHidingEnabled: Boolean = false,
val isInProgress: Boolean = false,
val selectedAppCurrency: AppCurrency = AppCurrency.Default,
val selectedThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
)
enum class SecurityOption { LongTap, PassCode, AccessCode }
enum class AppSetting {
RequireAccessCode, BiometricAuthentication,
}

View file

@ -0,0 +1,3 @@
package com.tangem.tap.features.details.redux
enum class SecurityOption { LongTap, PassCode, AccessCode }

View file

@ -1,11 +1,15 @@
package com.tangem.tap.features.details.ui.appsettings.model
import androidx.compose.runtime.Stable
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.appcurrency.model.AppCurrency
@ -13,42 +17,38 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
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.AppSettingsDialogsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState
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.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.extensions.addIf
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
@Stable
@ModelScoped
internal class AppSettingsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val appCurrencyRepository: AppCurrencyRepository,
appCurrencyRepository: AppCurrencyRepository,
private val walletsRepository: WalletsRepository,
private val userWalletsListRepository: UserWalletsListRepository,
private val balanceHidingRepository: BalanceHidingRepository,
@ -56,72 +56,100 @@ internal class AppSettingsModel @Inject constructor(
private val appThemeModeRepository: AppThemeModeRepository,
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
private val tangemSdkManager: TangemSdkManager,
private val settingsManager: SettingsManager,
private val settingsRepository: SettingsRepository,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val tangemHotSdk: TangemHotSdk,
private val router: Router,
private val uiMessageSender: UiMessageSender,
) : Model(), StoreSubscriber<DetailsState> {
) : Model() {
private val itemsFactory = AppSettingsItemsFactory()
private val dialogsFactory = AppSettingsDialogsFactory()
private val appCurrencyUpdatesJobHolder = JobHolder()
private val localState = MutableStateFlow(LocalState())
private val biometricsStatusJobHolder = JobHolder()
private val _uiState: MutableStateFlow<AppSettingsScreenState> = MutableStateFlow(
value = AppSettingsScreenState.Loading,
)
val uiState: StateFlow<AppSettingsScreenState> = _uiState
val uiState: StateFlow<AppSettingsScreenState>
field = MutableStateFlow<AppSettingsScreenState>(value = AppSettingsScreenState.Loading)
init {
bootstrapAppCurrencyUpdates()
bootstrapBiometricsUpdates()
bootstrapLocalState()
combine(
flow = appCurrencyRepository.getSelectedAppCurrency().distinctUntilChanged(),
flow2 = appThemeModeRepository.getAppThemeMode(),
flow3 = balanceHidingRepository.getBalanceHidingSettingsFlow(),
flow4 = localState,
) { currency, themeMode, hidingSettings, local ->
AppSettingsState(
appCurrency = currency,
themeMode = themeMode,
isHidingEnabled = hidingSettings.isHidingEnabledInSettings,
local = local,
)
}
.onEach { state ->
val items = buildItems(state)
uiState.update { prevState ->
when (prevState) {
is AppSettingsScreenState.Content -> prevState.copy(items = items)
is AppSettingsScreenState.Loading -> AppSettingsScreenState.Content(
items = items,
dialog = null,
)
}
}
}
.launchIn(modelScope)
subscribeToStoreChanges()
sendItemsAnalytics()
}
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 onResume() {
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(modelScope))
observeBiometricsStatusChanges()
}
override fun onDestroy() {
super.onDestroy()
store.unsubscribe(subscriber = this)
private fun observeBiometricsStatusChanges() {
flow {
do {
val isEnrollBiometricsNeeded = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull()
if (isEnrollBiometricsNeeded != null) {
emit(isEnrollBiometricsNeeded)
}
delay(timeMillis = 200)
} while (true)
}
.flowOn(dispatchers.default)
.distinctUntilChanged()
.onEach { isEnrollBiometricsNeeded ->
localState.update { it.copy(isEnrollBiometricsNeeded = isEnrollBiometricsNeeded) }
}
.launchIn(modelScope)
.saveIn(biometricsStatusJobHolder)
}
private fun buildItems(state: AppSettingsState): ImmutableList<AppSettingsScreenState.Item> {
val items = buildList {
addIf(
condition = state.needEnrollBiometrics,
condition = state.local.isEnrollBiometricsNeeded,
element = itemsFactory.createEnrollBiometricsCard(onClick = ::enrollBiometrics),
)
add(
itemsFactory.createSelectAppCurrencyButton(
currentAppCurrencyName = state.selectedAppCurrency.name,
currentAppCurrencyName = state.appCurrency.name,
onClick = ::showAppCurrencySelector,
),
)
val canUseBiometrics =
!state.needEnrollBiometrics && !state.isInProgress && state.hasSecuredWallets
val canUseBiometrics = with(state.local) {
!isEnrollBiometricsNeeded && !isInProgress && hasSecuredWallets
}
add(
itemsFactory.createUseBiometricsSwitch(
isChecked = state.useBiometricAuthentication,
isChecked = state.local.isBiometricAuthenticationUsed,
isEnabled = canUseBiometrics,
onCheckedChange = ::onBiometricAuthenticationToggled,
onDisabledClick = ::onBiometricAuthenticationDisabledClicked,
@ -130,8 +158,8 @@ internal class AppSettingsModel @Inject constructor(
add(
itemsFactory.createRequireAccessCodeSwitch(
isChecked = state.requireAccessCode || !state.useBiometricAuthentication,
isEnabled = canUseBiometrics && state.useBiometricAuthentication,
isChecked = state.local.isAccessCodeRequired || !state.local.isBiometricAuthenticationUsed,
isEnabled = canUseBiometrics && state.local.isBiometricAuthenticationUsed,
onCheckedChange = ::onRequireAccessCodeToggled,
),
)
@ -146,8 +174,8 @@ internal class AppSettingsModel @Inject constructor(
add(
itemsFactory.createSelectThemeModeButton(
currentThemeMode = state.selectedThemeMode,
onClick = { showThemeModeSelector(state.selectedThemeMode) },
currentThemeMode = state.themeMode,
onClick = { showThemeModeSelector(state.themeMode) },
),
)
}
@ -156,11 +184,12 @@ internal class AppSettingsModel @Inject constructor(
}
private fun enrollBiometrics() {
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics)
analyticsEventHandler.send(Settings.AppSettings.ButtonEnableBiometricAuthentication())
settingsManager.openBiometricSettings()
}
private fun showAppCurrencySelector() {
store.dispatchNavigationAction { push(AppRoute.AppCurrencySelector) }
router.push(AppRoute.AppCurrencySelector)
}
private fun showThemeModeSelector(selectedMode: AppThemeMode) {
@ -174,7 +203,7 @@ internal class AppSettingsModel @Inject constructor(
theme = AnalyticsParam.AppTheme.fromAppThemeMode(mode),
),
)
store.dispatchOnMain(DetailsAction.AppSettings.ChangeAppThemeMode(mode))
changeAppThemeMode(mode)
dismissDialog()
},
onDismiss = ::dismissDialog,
@ -188,13 +217,13 @@ internal class AppSettingsModel @Inject constructor(
// val param = AnalyticsParam.OnOffState(isChecked)
// analyticsEventHandler.send(Settings.AppSettings.BiometricAuthenticationChanged(param))
if (isChecked) {
onSettingsToggled(AppSetting.BiometricAuthentication, enable = true)
toggleBiometricsAuthentication(enable = true)
} else {
updateContentState {
copy(
dialog = dialogsFactory.createDisableBiometricAuthenticationAlert(
onDisable = {
onSettingsToggled(AppSetting.BiometricAuthentication, enable = false)
toggleBiometricsAuthentication(enable = false)
dismissDialog()
},
onDismiss = ::dismissDialog,
@ -217,7 +246,7 @@ internal class AppSettingsModel @Inject constructor(
dialog = if (isChecked) {
dialogsFactory.createEnableRequireAccessCodeAlert(
onEnable = {
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
toggleRequireAccessCode(enable = true)
dismissDialog()
},
onDismiss = ::dismissDialog,
@ -225,7 +254,7 @@ internal class AppSettingsModel @Inject constructor(
} else {
dialogsFactory.createDisableRequireAccessCodeAlert(
onDisable = {
onSettingsToggled(AppSetting.RequireAccessCode, enable = false)
toggleRequireAccessCode(enable = false)
dismissDialog()
},
onDismiss = ::dismissDialog,
@ -235,51 +264,125 @@ internal class AppSettingsModel @Inject constructor(
}
}
private fun onSettingsToggled(setting: AppSetting, enable: Boolean) {
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
private fun toggleBiometricsAuthentication(enable: Boolean) {
localState.update { it.copy(isBiometricAuthenticationUsed = enable, isInProgress = true) }
modelScope.launch {
// Nothing to change
if (walletsRepository.useBiometricAuthentication() == enable) {
localState.update { it.copy(isInProgress = false) }
return@launch
}
if (enable) {
setBiometricLockForAllWallets()
} else {
removeAllBiometricData()
walletsRepository.setRequireAccessCode(value = true)
localState.update { it.copy(isAccessCodeRequired = true) }
}
walletsRepository.setUseBiometricAuthentication(value = enable)
localState.update { it.copy(isInProgress = false) }
}
}
private fun toggleRequireAccessCode(enable: Boolean) {
localState.update { it.copy(isAccessCodeRequired = enable, isInProgress = true) }
modelScope.launch {
// Nothing to change
if (walletsRepository.requireAccessCode() == enable) {
localState.update { it.copy(isInProgress = false) }
return@launch
}
if (enable) {
removeAllBiometricSingData(userWalletsListRepository.userWalletsSync())
}
walletsRepository.setRequireAccessCode(value = enable)
localState.update { it.copy(isInProgress = false) }
}
}
private suspend fun setBiometricLockForAllWallets() {
val userWallets = userWalletsListRepository.userWalletsSync()
userWallets.forEach { wallet ->
userWalletsListRepository.setLock(
userWalletId = wallet.walletId,
lockMethod = UserWalletsListRepository.LockMethod.Biometric,
changeUnsecured = false,
)
}
}
private suspend fun removeAllBiometricData() {
val userWallets = userWalletsListRepository.userWalletsSync()
userWallets.forEach {
userWalletsListRepository.removeBiometricLock(it.walletId)
}
removeAllBiometricSingData(userWallets)
}
private suspend fun removeAllBiometricSingData(userWallets: List<UserWallet>) {
deleteSavedAccessCodes()
userWallets.forEach { wallet ->
if (wallet is UserWallet.Hot) {
userWalletsListRepository.saveWithoutLock(
userWallet = wallet.copy(
hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(wallet.hotWalletId),
),
)
}
}
}
private suspend fun deleteSavedAccessCodes() {
tangemSdkManager.clearSavedUserCodes()
.doOnSuccess {
analyticsEventHandler.send(
Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.Off),
)
settingsRepository.setShouldSaveAccessCodes(value = false)
cardSdkConfigRepository.setAccessCodeRequestPolicy(isBiometricsRequestPolicy = false)
}
.doOnFailure { error ->
TangemLogger.e("Unable to delete saved access codes", error)
}
}
private fun onFlipToHideBalanceToggled(enable: Boolean) {
val param = AnalyticsParam.OnOffState(enable)
analyticsEventHandler.send(Settings.AppSettings.HideBalanceChanged(param))
store.dispatch(DetailsAction.AppSettings.ChangeBalanceHiding(shouldHideBalance = enable))
modelScope.launch {
val settings = balanceHidingRepository.getBalanceHidingSettings().copy(
isHidingEnabledInSettings = enable,
isBalanceHidden = false,
)
balanceHidingRepository.storeBalanceHidingSettings(settings)
}
}
private fun changeAppThemeMode(mode: AppThemeMode) {
modelScope.launch {
appThemeModeRepository.changeAppThemeMode(mode)
}
}
private fun dismissDialog() {
updateContentState { copy(dialog = null) }
}
private fun bootstrapAppCurrencyUpdates() {
appCurrencyRepository
.getSelectedAppCurrency()
.distinctUntilChanged()
.onEach { appCurrency ->
store.dispatchWithMain(DetailsAction.AppSettings.ChangeAppCurrency(appCurrency))
}
.launchIn(scope)
.saveIn(appCurrencyUpdatesJobHolder)
}
private fun bootstrapBiometricsUpdates() = modelScope.launch {
val state = AppSettingsState(
useBiometricAuthentication = walletsRepository.useBiometricAuthentication(),
requireAccessCode = walletsRepository.requireAccessCode(),
isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings,
selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default,
selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT,
needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true,
hasSecuredWallets = userWalletsListRepository.hasSecuredWallets(),
)
store.dispatchWithMain(DetailsAction.AppSettings.Prepare(state))
}
private fun subscribeToStoreChanges() {
store.subscribe(subscriber = this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
private fun bootstrapLocalState() = modelScope.launch {
localState.update { state ->
state.copy(
hasSecuredWallets = userWalletsListRepository.hasSecuredWallets(),
isEnrollBiometricsNeeded = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true,
isBiometricAuthenticationUsed = walletsRepository.useBiometricAuthentication(),
isAccessCodeRequired = walletsRepository.requireAccessCode(),
)
}
}
@ -288,15 +391,30 @@ internal class AppSettingsModel @Inject constructor(
.filterIsInstance<AppSettingsScreenState.Content>()
.distinctUntilChangedBy(AppSettingsScreenState.Content::items)
.onEach { appSettingsItemsAnalyticsSender.send(it.items) }
.launchIn(scope)
.launchIn(modelScope)
}
private fun updateContentState(block: AppSettingsScreenState.Content.() -> AppSettingsScreenState.Content) {
_uiState.update { prevState ->
uiState.update { prevState ->
when (prevState) {
is AppSettingsScreenState.Content -> block(prevState)
is AppSettingsScreenState.Loading -> prevState
}
}
}
private data class LocalState(
val hasSecuredWallets: Boolean = false,
val isEnrollBiometricsNeeded: Boolean = false,
val isBiometricAuthenticationUsed: Boolean = false,
val isAccessCodeRequired: Boolean = false,
val isInProgress: Boolean = false,
)
private data class AppSettingsState(
val themeMode: AppThemeMode = AppThemeMode.DEFAULT,
val isHidingEnabled: Boolean = false,
val appCurrency: AppCurrency = AppCurrency.Default,
val local: LocalState = LocalState(),
)
}