diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index bc16aae955..092abd503b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -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), ) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index a6275955a1..d854ba1088 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -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> { return listOf( logMiddleware, - DetailsMiddleware().detailsMiddleware, LockUserWalletsTimerMiddleware().middleware, AccessCodeRequestPolicyMiddleware().middleware, DaggerGraphMiddleware.daggerGraphMiddleware, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt deleted file mode 100644 index 072d738717..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt deleted file mode 100644 index fa438bf186..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ /dev/null @@ -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 = { _, 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 { - 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) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt deleted file mode 100644 index 40549d69f4..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt deleted file mode 100644 index e209c707e4..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ /dev/null @@ -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, -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/SecurityOption.kt b/app/src/main/java/com/tangem/tap/features/details/redux/SecurityOption.kt new file mode 100644 index 0000000000..e55259df3b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/redux/SecurityOption.kt @@ -0,0 +1,3 @@ +package com.tangem.tap.features.details.redux + +enum class SecurityOption { LongTap, PassCode, AccessCode } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt index e65c6560da..c9edf25848 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt @@ -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 { +) : 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 = MutableStateFlow( - value = AppSettingsScreenState.Loading, - ) - val uiState: StateFlow = _uiState + val uiState: StateFlow + field = MutableStateFlow(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 { 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) { + 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() .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(), + ) } \ No newline at end of file