Updated on 2026-08-14
This commit is contained in:
parent
11d218a155
commit
5f2acac137
8 changed files with 214 additions and 493 deletions
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.tap.features.details.redux
|
||||
|
||||
enum class SecurityOption { LongTap, PassCode, AccessCode }
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue