Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-21 02:23:53 +03:00
parent 3bda096aad
commit 5e7b19f1c0
10 changed files with 189 additions and 127 deletions

View file

@ -45,11 +45,13 @@ sealed class DetailsAction : Action {
sealed class AppSettings : DetailsAction() {
data class SwitchPrivacySetting(
val enable: Boolean,
val setting: PrivacySetting,
val setting: AppSetting,
) : AppSettings() {
data class Success(
val enable: Boolean,
val setting: PrivacySetting,
object Success : AppSettings()
data class Failure(
val prevState: Boolean,
val setting: AppSetting,
) : AppSettings()
}

View file

@ -6,6 +6,7 @@ import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
@ -207,8 +208,8 @@ class DetailsMiddleware {
when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
when (action.setting) {
PrivacySetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable)
PrivacySetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable)
AppSetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable)
AppSetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable)
}
}
is DetailsAction.AppSettings.CheckBiometricsStatus -> {
@ -218,6 +219,7 @@ class DetailsMiddleware {
enrollBiometrics()
}
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
is DetailsAction.AppSettings.BiometricsStatusChanged,
-> Unit
}
@ -230,7 +232,7 @@ class DetailsMiddleware {
private fun checkBiometricsStatus(awaitStatusChange: Boolean, state: DetailsState) {
scope.launch {
if (awaitStatusChange) {
while (state.needEnrollBiometrics == tangemSdkManager.needEnrollBiometrics) {
while (state.appSettingsState.needEnrollBiometrics == tangemSdkManager.needEnrollBiometrics) {
delay(timeMillis = 100)
}
}
@ -248,47 +250,95 @@ class DetailsMiddleware {
}
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
if (state.saveWallets == enable) return@launch
if (enable) {
saveCurrentWallet(state)
} else {
deleteSavedWallets()
if (state.saveAccessCodes) {
deleteSavedAccessCodes()
// Nothing to change
if (preferencesStorage.shouldSaveUserWallets == enable) {
store.dispatchOnMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
toggleSaveWallets(state.scanResponse, enable)
.doOnFailure {
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Failure(
prevState = !enable,
setting = AppSetting.SaveWallets,
),
)
}
.doOnSuccess {
store.dispatchOnMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private suspend fun toggleSaveWallets(
scanResponse: ScanResponse?,
enable: Boolean,
): CompletionResult<Unit> {
return if (enable) {
saveCurrentWallet(scanResponse, enableAccessCodesSaving = false)
} else {
deleteSavedWalletsAndAccessCodes()
}
}
private fun toggleSaveAccessCodes(state: DetailsState, enable: Boolean) = scope.launch {
if (state.saveAccessCodes == enable) return@launch
if (enable) {
saveAccessCodes(state)
if (!state.saveWallets) {
saveCurrentWallet(state)
// Nothing to change
if (preferencesStorage.shouldSaveAccessCodes == enable) {
store.dispatchOnMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
toggleSaveAccessCodes(state.scanResponse, state.appSettingsState.saveWallets, enable)
.doOnFailure {
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Failure(
prevState = !enable,
setting = AppSetting.SaveAccessCode,
),
)
}
.doOnSuccess {
store.dispatchOnMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private suspend fun toggleSaveAccessCodes(
scanResponse: ScanResponse?,
isWalletsSavingEnabled: Boolean,
enable: Boolean,
): CompletionResult<Unit> {
return if (enable) {
if (!isWalletsSavingEnabled) {
saveCurrentWallet(scanResponse, enableAccessCodesSaving = true)
} else {
saveAccessCodes(scanResponse)
}
} else {
deleteSavedAccessCodes()
}
}
private suspend fun saveCurrentWallet(state: DetailsState) {
val scanResponse = state.scanResponse ?: return
val userWallet = UserWalletBuilder(scanResponse).build() ?: return
private suspend fun saveCurrentWallet(
scanResponse: ScanResponse?,
enableAccessCodesSaving: Boolean,
): CompletionResult<Unit> {
val userWallet = scanResponse?.let { UserWalletBuilder(it).build() }
?: return CompletionResult.Failure(TangemSdkError.ExceptionError(IllegalStateException()))
userWalletsListManager.save(userWallet)
return userWalletsListManager.save(userWallet)
.flatMap {
if (enableAccessCodesSaving) {
saveAccessCodes(scanResponse)
} else {
CompletionResult.Success(Unit)
}
}
.doOnSuccess {
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.On))
preferencesStorage.shouldShowSaveUserWalletScreen = false
preferencesStorage.shouldSaveUserWallets = true
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveWallets,
enable = true,
),
)
store.onUserWalletSelected(userWallet)
}
.doOnFailure { error ->
@ -296,21 +346,14 @@ class DetailsMiddleware {
}
}
private suspend fun deleteSavedWallets() {
userWalletsListManager.clear()
private suspend fun deleteSavedWalletsAndAccessCodes(): CompletionResult<Unit> {
return userWalletsListManager.clear()
.flatMap { walletStoresManager.clear() }
.doOnSuccess {
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off))
deleteSavedAccessCodes()
preferencesStorage.shouldSaveUserWallets = false
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveWallets,
enable = false,
),
)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
.doOnFailure { error ->
@ -318,24 +361,19 @@ class DetailsMiddleware {
}
}
private fun saveAccessCodes(state: DetailsState) {
private fun saveAccessCodes(scanResponse: ScanResponse?): CompletionResult<Unit> {
Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.On))
preferencesStorage.shouldSaveAccessCodes = true
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = state.scanResponse?.card?.isAccessCodeSet == true,
useBiometricsForAccessCode = scanResponse?.card?.isAccessCodeSet == true,
)
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveAccessCode,
enable = true,
),
)
return CompletionResult.Success(Unit)
}
private suspend fun deleteSavedAccessCodes() {
tangemSdkManager.clearSavedUserCodes()
private suspend fun deleteSavedAccessCodes(): CompletionResult<Unit> {
return tangemSdkManager.clearSavedUserCodes()
.doOnSuccess {
Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.Off))
@ -343,16 +381,6 @@ class DetailsMiddleware {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = false,
)
tangemSdkManager.clearSavedUserCodes()
.doOnSuccess {
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveAccessCode,
enable = false,
),
)
}
}
.doOnFailure { error ->
Timber.e(error, "Unable to delete saved access codes")

View file

@ -54,9 +54,11 @@ private fun handlePrepareScreen(
wallets = action.wallets,
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
appCurrency = store.state.globalState.appCurrency,
isBiometricsAvailable = tangemSdkManager.canUseBiometry,
saveWallets = preferencesStorage.shouldSaveUserWallets,
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
appSettingsState = AppSettingsState(
isBiometricsAvailable = tangemSdkManager.canUseBiometry,
saveWallets = preferencesStorage.shouldSaveUserWallets,
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
),
)
}
@ -162,14 +164,41 @@ private fun handlePrivacyAction(
state: DetailsState,
): DetailsState {
return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> when (action.setting) {
PrivacySetting.SaveWallets -> state.copy(saveWallets = action.enable)
PrivacySetting.SaveAccessCode -> state.copy(saveAccessCodes = action.enable)
}
is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy(
needEnrollBiometrics = action.needEnrollBiometrics,
is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy(
appSettingsState = when (action.setting) {
AppSetting.SaveWallets -> state.appSettingsState.copy(
isInProgress = true,
saveWallets = action.enable,
)
AppSetting.SaveAccessCode -> state.appSettingsState.copy(
isInProgress = true,
saveWallets = true, // User can't enable access codes saving without wallets saving
saveAccessCodes = 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.SaveWallets -> state.appSettingsState.copy(
isInProgress = false,
saveWallets = action.prevState,
)
AppSetting.SaveAccessCode -> state.appSettingsState.copy(
isInProgress = false,
saveAccessCodes = action.prevState,
)
},
)
is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy(
appSettingsState = state.appSettingsState.copy(
needEnrollBiometrics = action.needEnrollBiometrics,
),
)
is DetailsAction.AppSettings.SwitchPrivacySetting,
is DetailsAction.AppSettings.EnrollBiometrics,
is DetailsAction.AppSettings.CheckBiometricsStatus,
-> state

View file

@ -18,10 +18,7 @@ data class DetailsState(
val privacyPolicyUrl: String? = null,
val createBackupAllowed: Boolean = false,
val appCurrency: FiatCurrency = FiatCurrency.Default,
val saveWallets: Boolean = false,
val saveAccessCodes: Boolean = false,
val isBiometricsAvailable: Boolean = false,
val needEnrollBiometrics: Boolean = false,
val appSettingsState: AppSettingsState = AppSettingsState(),
) : StateType {
// if you do not delegate - the application crashes on startup,
@ -54,8 +51,16 @@ data class ManageSecurityState(
val buttonProceed: Button = Button(true),
)
data class AppSettingsState(
val saveWallets: Boolean = false,
val saveAccessCodes: Boolean = false,
val isBiometricsAvailable: Boolean = false,
val needEnrollBiometrics: Boolean = false,
val isInProgress: Boolean = false,
)
enum class SecurityOption { LongTap, PassCode, AccessCode }
enum class PrivacySetting {
enum class AppSetting {
SaveWallets, SaveAccessCode
}

View file

@ -23,7 +23,7 @@ import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerW32
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.redux.PrivacySetting
import com.tangem.tap.features.details.redux.AppSetting
import com.tangem.tap.features.details.ui.appsettings.components.EnrollBiometricsCard
import com.tangem.tap.features.details.ui.appsettings.components.SettingsAlertDialog
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
@ -41,8 +41,8 @@ fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit) {
@Composable
private fun AppSettings(state: AppSettingsScreenState) {
var dialogType by remember { mutableStateOf<PrivacySetting?>(null) }
val onDialogStateChange: (PrivacySetting?) -> Unit = { dialogType = it }
var dialogType by remember { mutableStateOf<AppSetting?>(null) }
val onDialogStateChange: (AppSetting?) -> Unit = { dialogType = it }
dialogType?.let {
SettingsAlertDialog(
@ -60,13 +60,13 @@ private fun AppSettings(state: AppSettingsScreenState) {
AppSettingsElement(
state = state,
setting = PrivacySetting.SaveWallets,
setting = AppSetting.SaveWallets,
onDialogStateChange = onDialogStateChange,
)
SpacerH32()
AppSettingsElement(
state = state,
setting = PrivacySetting.SaveAccessCode,
setting = AppSetting.SaveAccessCode,
onDialogStateChange = onDialogStateChange,
)
}
@ -76,16 +76,16 @@ private fun AppSettings(state: AppSettingsScreenState) {
@Composable
private fun AppSettingsElement(
state: AppSettingsScreenState,
setting: PrivacySetting,
onDialogStateChange: (PrivacySetting?) -> Unit,
setting: AppSetting,
onDialogStateChange: (AppSetting?) -> Unit,
) {
val titleRes = when (setting) {
PrivacySetting.SaveWallets -> R.string.app_settings_saved_wallet
PrivacySetting.SaveAccessCode -> R.string.app_settings_saved_access_codes
AppSetting.SaveWallets -> R.string.app_settings_saved_wallet
AppSetting.SaveAccessCode -> R.string.app_settings_saved_access_codes
}
val subtitleRes = when (setting) {
PrivacySetting.SaveWallets -> R.string.app_settings_saved_wallet_footer
PrivacySetting.SaveAccessCode -> R.string.app_settings_saved_access_codes_footer
AppSetting.SaveWallets -> R.string.app_settings_saved_wallet_footer
AppSetting.SaveAccessCode -> R.string.app_settings_saved_access_codes_footer
}
val checked = state.settings[setting] ?: false
@ -144,10 +144,10 @@ private fun AppSettingsElement(
}
private fun onCheckedChange(
element: PrivacySetting,
element: AppSetting,
enabled: Boolean,
onSettingToggled: (PrivacySetting, Boolean) -> Unit,
onDialogStateChange: (PrivacySetting?) -> Unit,
onSettingToggled: (AppSetting, Boolean) -> Unit,
onDialogStateChange: (AppSetting?) -> Unit,
) {
// Show warning if user wants to disable the switch
if (!enabled) {
@ -169,8 +169,8 @@ private fun AppSettingsScreenSample(
AppSettingsScreen(
state = AppSettingsScreenState(
settings = mapOf(
PrivacySetting.SaveWallets to true,
PrivacySetting.SaveAccessCode to false,
AppSetting.SaveWallets to true,
AppSetting.SaveAccessCode to false,
),
showEnrollBiometricsCard = false,
isTogglesEnabled = true,
@ -206,8 +206,8 @@ private fun AppSettingsScreen_EnrollBiometrics_Sample(
AppSettingsScreen(
state = AppSettingsScreenState(
settings = mapOf(
PrivacySetting.SaveWallets to true,
PrivacySetting.SaveAccessCode to false,
AppSetting.SaveWallets to true,
AppSetting.SaveAccessCode to false,
),
showEnrollBiometricsCard = true,
isTogglesEnabled = false,

View file

@ -1,11 +1,11 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.tap.features.details.redux.PrivacySetting
import com.tangem.tap.features.details.redux.AppSetting
data class AppSettingsScreenState(
val settings: Map<PrivacySetting, Boolean> = emptyMap(),
val settings: Map<AppSetting, Boolean> = emptyMap(),
val showEnrollBiometricsCard: Boolean = false,
val isTogglesEnabled: Boolean = true,
val onSettingToggled: (PrivacySetting, Boolean) -> Unit = { _, _ -> /* no-op */ },
val isTogglesEnabled: Boolean = false,
val onSettingToggled: (AppSetting, Boolean) -> Unit = { _, _ -> /* no-op */ },
val onEnrollBiometrics: () -> Unit = { /* no-op */ },
)

View file

@ -2,35 +2,33 @@ package com.tangem.tap.features.details.ui.appsettings
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.AppSetting
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.PrivacySetting
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.rekotlin.Store
class AppSettingsViewModel(private val store: Store<AppState>) {
private val _uiState = MutableStateFlow(updateState(store.state.detailsState))
val uiState: StateFlow<AppSettingsScreenState> = _uiState
fun updateState(state: DetailsState): AppSettingsScreenState {
return AppSettingsScreenState(
settings = mapOf(
PrivacySetting.SaveWallets to state.saveWallets,
PrivacySetting.SaveAccessCode to state.saveAccessCodes,
),
showEnrollBiometricsCard = state.needEnrollBiometrics,
isTogglesEnabled = !state.needEnrollBiometrics,
onSettingToggled = { privacySetting, enabled ->
onSettingsToggled(privacySetting, enabled)
},
onEnrollBiometrics = {
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics)
},
)
return with(state.appSettingsState) {
AppSettingsScreenState(
settings = mapOf(
AppSetting.SaveWallets to saveWallets,
AppSetting.SaveAccessCode to saveAccessCodes,
),
showEnrollBiometricsCard = needEnrollBiometrics,
isTogglesEnabled = !needEnrollBiometrics && !isInProgress,
onSettingToggled = { privacySetting, enabled ->
onSettingsToggled(privacySetting, enabled)
},
onEnrollBiometrics = {
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics)
},
)
}
}
private fun onSettingsToggled(setting: PrivacySetting, enable: Boolean) {
private fun onSettingsToggled(setting: AppSetting, enable: Boolean) {
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
}

View file

@ -12,18 +12,18 @@ import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.TextButton
import com.tangem.core.ui.components.WarningTextButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.redux.PrivacySetting
import com.tangem.tap.features.details.redux.AppSetting
import com.tangem.wallet.R
@Composable
internal fun SettingsAlertDialog(
element: PrivacySetting,
onDialogStateChange: (PrivacySetting?) -> Unit,
onSettingToggle: () -> Unit
element: AppSetting,
onDialogStateChange: (AppSetting?) -> Unit,
onSettingToggle: () -> Unit,
) {
val text = when (element) {
PrivacySetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message
PrivacySetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message
AppSetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message
AppSetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message
}
AlertDialog(
@ -74,7 +74,7 @@ private fun SettingsAlertDialogSample(
.background(TangemTheme.colors.background.primary),
) {
SettingsAlertDialog(
element = PrivacySetting.SaveAccessCode,
element = AppSetting.SaveAccessCode,
onDialogStateChange = {},
onSettingToggle = { },
)

View file

@ -47,7 +47,8 @@ fun TangemSwitch(
},
label = "",
) { isChecked ->
if (isChecked) checkedColor else uncheckedColor
(if (isChecked) checkedColor else uncheckedColor)
.copy(alpha = if (enabled) 1f else .4f)
}
val interactionSource = remember { MutableInteractionSource() }

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.details.ui.details
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.SupportInfo
@ -38,7 +37,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
SettingsElement.PrivacyPolicy -> {
if (state.privacyPolicyUrl != null) it else null
}
SettingsElement.AppSettings -> if (state.isBiometricsAvailable) it else null
SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null
SettingsElement.AppCurrency -> if (cardTypesResolver?.isMultiwalletAllowed() != true) it else null
// SettingsElement.ReferralProgram -> if (state.scanResponse?.card?.isTangemWallet == true) it else null
else -> it