BITAU-181 Allow user to update default save options from settings (#252)

This commit is contained in:
Andrew Haisting
2024-10-24 09:49:07 -05:00
committed by GitHub
parent 5fdfb26950
commit 1d2095b0b3
15 changed files with 360 additions and 2 deletions

View File

@@ -2,6 +2,7 @@ package com.bitwarden.authenticator.data.platform.datasource.disk
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppLanguage
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import kotlinx.coroutines.flow.Flow
/**
@@ -24,6 +25,11 @@ interface SettingsDiskSource {
*/
val appThemeFlow: Flow<AppTheme>
/**
* The currently persisted default save option.
*/
var defaultSaveOption: DefaultSaveOption
/**
* The currently persisted biometric integrity source for the system.
*/

View File

@@ -5,11 +5,13 @@ import com.bitwarden.authenticator.data.platform.datasource.disk.BaseDiskSource.
import com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppLanguage
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription
private const val APP_THEME_KEY = "$BASE_KEY:theme"
private const val APP_LANGUAGE_KEY = "$BASE_KEY:appLocale"
private const val DEFAULT_SAVE_OPTION_KEY = "$BASE_KEY:defaultSaveOption"
private const val SYSTEM_BIOMETRIC_INTEGRITY_SOURCE_KEY = "$BASE_KEY:biometricIntegritySource"
private const val ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY = "$BASE_KEY:accountBiometricIntegrityValid"
private const val ALERT_THRESHOLD_SECONDS_KEY = "$BASE_KEY:alertThresholdSeconds"
@@ -74,6 +76,19 @@ class SettingsDiskSourceImpl(
get() = mutableAppThemeFlow
.onSubscription { emit(appTheme) }
override var defaultSaveOption: DefaultSaveOption
get() = getString(key = DEFAULT_SAVE_OPTION_KEY)
?.let { storedValue ->
DefaultSaveOption.entries.firstOrNull { storedValue == it.value }
}
?: DefaultSaveOption.NONE
set(newValue) {
putString(
key = DEFAULT_SAVE_OPTION_KEY,
value = newValue.value,
)
}
override var systemBiometricIntegritySource: String?
get() = getString(key = SYSTEM_BIOMETRIC_INTEGRITY_SOURCE_KEY)
set(value) {

View File

@@ -3,6 +3,7 @@ package com.bitwarden.authenticator.data.platform.repository
import com.bitwarden.authenticator.data.platform.repository.model.BiometricsKeyResult
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppLanguage
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -31,6 +32,11 @@ interface SettingsRepository {
*/
var authenticatorAlertThresholdSeconds: Int
/**
* The currently stored [DefaultSaveOption].
*/
var defaultSaveOption: DefaultSaveOption
/**
* Whether or not biometric unlocking is enabled for the current user.
*/

View File

@@ -9,6 +9,7 @@ import com.bitwarden.authenticator.data.platform.manager.DispatcherManager
import com.bitwarden.authenticator.data.platform.repository.model.BiometricsKeyResult
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppLanguage
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
@@ -41,6 +42,8 @@ class SettingsRepositoryImpl(
override var authenticatorAlertThresholdSeconds = settingsDiskSource.getAlertThresholdSeconds()
override var defaultSaveOption: DefaultSaveOption by settingsDiskSource::defaultSaveOption
override val isUnlockWithBiometricsEnabled: Boolean
get() = authDiskSource.getUserBiometricUnlockKey() != null

View File

@@ -31,7 +31,11 @@ import com.bitwarden.authenticator.ui.platform.components.util.maxDialogHeight
* Displays a dialog with a title and "Cancel" button.
*
* @param title Title to display.
* @param subtitle Optional subtitle to display below the title.
* @param dismissLabel Label to show on the dismiss button at the bottom of the dialog.
* @param onDismissRequest Invoked when the user dismisses the dialog.
* @param onDismissActionClick Invoked when the user dismisses the via the dismiss action button.
* By default, this just defers to onDismissRequest.
* @param selectionItems Lambda containing selection items to show to the user. See
* [BitwardenSelectionRow].
*/
@@ -40,7 +44,10 @@ import com.bitwarden.authenticator.ui.platform.components.util.maxDialogHeight
@Composable
fun BitwardenSelectionDialog(
title: String,
subtitle: String? = null,
dismissLabel: String = stringResource(R.string.cancel),
onDismissRequest: () -> Unit,
onDismissActionClick: () -> Unit = onDismissRequest,
selectionItems: @Composable ColumnScope.() -> Unit = {},
) {
Dialog(
@@ -69,6 +76,20 @@ fun BitwardenSelectionDialog(
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.headlineSmall,
)
subtitle?.let {
Text(
modifier = Modifier
.padding(
start = 24.dp,
end = 24.dp,
bottom = 24.dp,
)
.fillMaxWidth(),
text = subtitle,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
)
}
if (scrollState.canScrollBackward) {
Box(
modifier = Modifier
@@ -93,8 +114,8 @@ fun BitwardenSelectionDialog(
}
BitwardenTextButton(
modifier = Modifier.padding(24.dp),
label = stringResource(id = R.string.cancel),
onClick = onDismissRequest,
label = dismissLabel,
onClick = onDismissActionClick,
)
}
}

View File

@@ -1,3 +1,5 @@
@file:Suppress("TooManyFunctions")
package com.bitwarden.authenticator.ui.platform.feature.settings
import android.content.Intent
@@ -60,6 +62,7 @@ import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaf
import com.bitwarden.authenticator.ui.platform.components.toggle.BitwardenWideSwitch
import com.bitwarden.authenticator.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import com.bitwarden.authenticator.ui.platform.manager.biometrics.BiometricsManager
import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManager
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
@@ -174,6 +177,15 @@ fun SettingsScreen(
viewModel.trySendAction(SettingsAction.DataClick.SyncWithBitwardenClick)
}
},
onDefaultSaveOptionUpdated = remember(viewModel) {
{
viewModel.trySendAction(
SettingsAction.DataClick.DefaultSaveOptionUpdated(it),
)
}
},
defaultSaveOption = state.defaultSaveOption,
shouldShowDefaultSaveOptions = state.showDefaultSaveOptionRow,
shouldShowSyncWithBitwardenApp = state.showSyncWithBitwarden,
)
Spacer(modifier = Modifier.height(16.dp))
@@ -265,11 +277,14 @@ private fun SecuritySettings(
@Suppress("LongMethod")
private fun VaultSettings(
modifier: Modifier = Modifier,
defaultSaveOption: DefaultSaveOption,
onExportClick: () -> Unit,
onImportClick: () -> Unit,
onBackupClick: () -> Unit,
onSyncWithBitwardenClick: () -> Unit,
onDefaultSaveOptionUpdated: (DefaultSaveOption) -> Unit,
shouldShowSyncWithBitwardenApp: Boolean,
shouldShowDefaultSaveOptions: Boolean,
) {
BitwardenListHeaderText(
modifier = Modifier.padding(horizontal = 16.dp),
@@ -341,6 +356,58 @@ private fun VaultSettings(
},
)
}
if (shouldShowDefaultSaveOptions) {
DefaultSaveOptionSelectionRow(
currentSelection = defaultSaveOption,
onSaveOptionUpdated = onDefaultSaveOptionUpdated,
)
}
}
@Composable
private fun DefaultSaveOptionSelectionRow(
currentSelection: DefaultSaveOption,
onSaveOptionUpdated: (DefaultSaveOption) -> Unit,
modifier: Modifier = Modifier,
) {
var shouldShowDefaultSaveOptionDialog by remember { mutableStateOf(false) }
BitwardenTextRow(
text = stringResource(id = R.string.default_save_options),
onClick = { shouldShowDefaultSaveOptionDialog = true },
modifier = modifier,
withDivider = true,
) {
Text(
modifier = Modifier.padding(vertical = 20.dp),
text = currentSelection.displayLabel(),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
var dialogSelection by remember { mutableStateOf(currentSelection) }
if (shouldShowDefaultSaveOptionDialog) {
BitwardenSelectionDialog(
title = stringResource(id = R.string.default_save_options),
subtitle = stringResource(id = R.string.default_save_options_subtitle),
dismissLabel = stringResource(id = R.string.confirm),
onDismissRequest = { shouldShowDefaultSaveOptionDialog = false },
onDismissActionClick = {
onSaveOptionUpdated(dialogSelection)
shouldShowDefaultSaveOptionDialog = false
},
) {
DefaultSaveOption.entries.forEach { option ->
BitwardenSelectionRow(
text = option.displayLabel,
isSelected = option == dialogSelection,
onClick = {
dialogSelection = DefaultSaveOption.entries.first { it == option }
},
)
}
}
}
}
@Composable

View File

@@ -18,6 +18,7 @@ import com.bitwarden.authenticator.ui.platform.base.util.asText
import com.bitwarden.authenticator.ui.platform.base.util.concat
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppLanguage
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -53,6 +54,7 @@ class SettingsViewModel @Inject constructor(
isSyncWithBitwardenFeatureEnabled =
featureFlagManager.getFeatureFlag(LocalFeatureFlag.PasswordManagerSync),
accountSyncState = authenticatorBridgeManager.accountSyncStateFlow.value,
defaultSaveOption = settingsRepository.defaultSaveOption,
),
) {
override fun handleAction(action: SettingsAction) {
@@ -141,6 +143,19 @@ class SettingsViewModel @Inject constructor(
SettingsAction.DataClick.ImportClick -> handleImportClick()
SettingsAction.DataClick.BackupClick -> handleBackupClick()
SettingsAction.DataClick.SyncWithBitwardenClick -> handleSyncWithBitwardenClick()
is SettingsAction.DataClick.DefaultSaveOptionUpdated ->
handleDefaultSaveOptionUpdated(action)
}
}
private fun handleDefaultSaveOptionUpdated(
action: SettingsAction.DataClick.DefaultSaveOptionUpdated,
) {
settingsRepository.defaultSaveOption = action.option
mutableStateFlow.update {
it.copy(
defaultSaveOption = action.option,
)
}
}
@@ -253,6 +268,7 @@ class SettingsViewModel @Inject constructor(
clock: Clock,
appLanguage: AppLanguage,
appTheme: AppTheme,
defaultSaveOption: DefaultSaveOption,
unlockWithBiometricsEnabled: Boolean,
isSubmitCrashLogsEnabled: Boolean,
accountSyncState: AccountSyncState,
@@ -275,7 +291,9 @@ class SettingsViewModel @Inject constructor(
.asText()
.concat(": ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})".asText()),
copyrightInfo = copyrightInfo,
defaultSaveOption = defaultSaveOption,
showSyncWithBitwarden = shouldShowSyncWithBitwarden,
showDefaultSaveOptionRow = shouldShowSyncWithBitwarden,
)
}
}
@@ -287,9 +305,11 @@ class SettingsViewModel @Inject constructor(
@Parcelize
data class SettingsState(
val appearance: Appearance,
val defaultSaveOption: DefaultSaveOption,
val isUnlockWithBiometricsEnabled: Boolean,
val isSubmitCrashLogsEnabled: Boolean,
val showSyncWithBitwarden: Boolean,
val showDefaultSaveOptionRow: Boolean,
val dialog: Dialog?,
val version: Text,
val copyrightInfo: Text,
@@ -419,6 +439,11 @@ sealed class SettingsAction(
* Indicates the user clicked sync with Bitwarden.
*/
data object SyncWithBitwardenClick : DataClick()
/**
* User confirmed a new [DeafultSaveOption].
*/
data class DefaultSaveOptionUpdated(val option: DefaultSaveOption) : DataClick()
}
/**

View File

@@ -0,0 +1,12 @@
package com.bitwarden.authenticator.ui.platform.feature.settings.data.model
/**
* Represents the default save location the user has set.
*
* The [value] is used for consistent storage purposes.
*/
enum class DefaultSaveOption(val value: String?) {
BITWARDEN_APP(value = "bitwarden"),
LOCAL(value = "local"),
NONE(value = null),
}

View File

@@ -0,0 +1,16 @@
package com.bitwarden.authenticator.ui.platform.util
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.ui.platform.base.util.Text
import com.bitwarden.authenticator.ui.platform.base.util.asText
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
/**
* Returns a human-readable display label for the given [DefaultSaveOption].
*/
val DefaultSaveOption.displayLabel: Text
get() = when (this) {
DefaultSaveOption.NONE -> R.string.none.asText()
DefaultSaveOption.LOCAL -> R.string.save_locally.asText()
DefaultSaveOption.BITWARDEN_APP -> R.string.save_to_bitwarden.asText()
}

View File

@@ -133,4 +133,10 @@
<string name="something_went_wrong">Something went wrong</string>
<string name="please_try_again">Please try again</string>
<string name="move_to_bitwarden">Move to Bitwarden</string>
<string name="default_save_options">Default save options</string>
<string name="save_to_bitwarden">Save to Bitwarden</string>
<string name="save_locally">Save locally</string>
<string name="none">None</string>
<string name="default_save_options_subtitle">Select where you would like to save new verification codes.</string>
<string name="confirm">Confirm</string>
</resources>

View File

@@ -2,6 +2,8 @@ package com.bitwarden.authenticator.data.platform.datasource.disk
import androidx.core.content.edit
import com.bitwarden.authenticator.data.platform.base.FakeSharedPreferences
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
@@ -50,4 +52,47 @@ class SettingDiskSourceTest {
}
assertTrue(settingDiskSource.hasUserDismissedSyncWithBitwardenCard!!)
}
@Test
fun `defaultSaveOption should read and write from shared preferences`() {
val sharedPrefsKey = "bwPreferencesStorage:defaultSaveOption"
// Verify initial value is null and disk source should default to NONE
assertNull(sharedPreferences.getString(sharedPrefsKey, null))
assertEquals(
DefaultSaveOption.NONE,
settingDiskSource.defaultSaveOption,
)
// Updating the shared preferences should update disk source
sharedPreferences.edit {
putString(
sharedPrefsKey,
DefaultSaveOption.BITWARDEN_APP.value,
)
}
assertEquals(
DefaultSaveOption.BITWARDEN_APP,
settingDiskSource.defaultSaveOption,
)
// Updating the disk source should update shared preferences
settingDiskSource.defaultSaveOption = DefaultSaveOption.LOCAL
assertEquals(
DefaultSaveOption.LOCAL.value,
sharedPreferences.getString(sharedPrefsKey, null),
)
// Incorrect value should default to DefaultSaveOption.NONE
sharedPreferences.edit {
putString(
sharedPrefsKey,
"invalid",
)
}
assertEquals(
DefaultSaveOption.NONE,
settingDiskSource.defaultSaveOption,
)
}
}

View File

@@ -6,11 +6,13 @@ import com.bitwarden.authenticator.data.authenticator.datasource.sdk.Authenticat
import com.bitwarden.authenticator.data.platform.base.FakeDispatcherManager
import com.bitwarden.authenticator.data.platform.datasource.disk.SettingsDiskSource
import com.bitwarden.authenticator.data.platform.manager.BiometricsEncryptionManager
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
@@ -74,4 +76,20 @@ class SettingsRepositoryTest {
settingsRepository.hasUserDismissedSyncWithBitwardenCard = true
verify { settingsRepository.hasUserDismissedSyncWithBitwardenCard = true }
}
@Test
fun `defaultSaveOption should pull from and update SettingsDiskSource`() {
// Reading from repository should read from disk source:
every { settingsDiskSource.defaultSaveOption } returns DefaultSaveOption.NONE
assertEquals(
DefaultSaveOption.NONE,
settingsRepository.defaultSaveOption,
)
verify { settingsDiskSource.defaultSaveOption }
// Writing to repository should write to disk source:
every { settingsDiskSource.defaultSaveOption = DefaultSaveOption.BITWARDEN_APP } just runs
settingsRepository.defaultSaveOption = DefaultSaveOption.BITWARDEN_APP
verify { settingsDiskSource.defaultSaveOption = DefaultSaveOption.BITWARDEN_APP }
}
}

View File

@@ -1,6 +1,11 @@
package com.bitwarden.authenticator.ui.platform.feature.settings
import android.content.Intent
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
@@ -13,6 +18,7 @@ import com.bitwarden.authenticator.ui.platform.base.util.asText
import com.bitwarden.authenticator.ui.platform.base.util.concat
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppLanguage
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import com.bitwarden.authenticator.ui.platform.manager.biometrics.BiometricsManager
import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManager
import io.mockk.every
@@ -24,6 +30,8 @@ import io.mockk.verify
import org.junit.Test
import org.junit.Before
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
class SettingsScreenTest : BaseComposeTest() {
@@ -109,10 +117,65 @@ class SettingsScreenTest : BaseComposeTest() {
)
}
}
@Test
fun `Default Save Options row should be hidden when showDefaultSaveOptionRow is false`() {
mutableStateFlow.value = DEFAULT_STATE
composeTestRule.onNodeWithText("Default save options").assertExists()
mutableStateFlow.update {
it.copy(
showDefaultSaveOptionRow = false,
)
}
composeTestRule.onNodeWithText("Default save options").assertDoesNotExist()
}
@Test
@Suppress("MaxLineLength")
fun `Default Save Options dialog should send DefaultSaveOptionUpdated when confirm is clicked`() =
runTest {
val expectedSaveOption = DefaultSaveOption.BITWARDEN_APP
mutableStateFlow.value = DEFAULT_STATE
composeTestRule
.onNodeWithText("Default save options")
.performScrollTo()
.performClick()
// Make sure the dialog is showing:
composeTestRule
.onAllNodesWithText("Default save options")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
// Select updated option:
composeTestRule
.onNodeWithText("Save to Bitwarden")
.assertIsDisplayed()
.performClick()
// Click confirm:
composeTestRule
.onNodeWithText("Confirm")
.assertIsDisplayed()
.performClick()
verify {
viewModel.trySendAction(
SettingsAction.DataClick.DefaultSaveOptionUpdated(expectedSaveOption),
)
}
// Make sure the dialog is not showing:
composeTestRule
.onNode(isDialog())
.assertDoesNotExist()
}
}
private val APP_LANGUAGE = AppLanguage.ENGLISH
private val APP_THEME = AppTheme.DEFAULT
private val DEFAULT_SAVE_OPTION = DefaultSaveOption.NONE
private val DEFAULT_STATE = SettingsState(
appearance = SettingsState.Appearance(
APP_LANGUAGE,
@@ -121,6 +184,8 @@ private val DEFAULT_STATE = SettingsState(
isSubmitCrashLogsEnabled = true,
isUnlockWithBiometricsEnabled = true,
showSyncWithBitwarden = true,
showDefaultSaveOptionRow = true,
defaultSaveOption = DEFAULT_SAVE_OPTION,
dialog = null,
version = R.string.version.asText()
.concat(": ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})".asText()),

View File

@@ -13,10 +13,14 @@ import com.bitwarden.authenticator.ui.platform.base.util.asText
import com.bitwarden.authenticator.ui.platform.base.util.concat
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppLanguage
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
@@ -33,6 +37,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
private val settingsRepository: SettingsRepository = mockk {
every { appLanguage } returns APP_LANGUAGE
every { appTheme } returns APP_THEME
every { defaultSaveOption } returns DEFAULT_SAVE_OPTION
every { isUnlockWithBiometricsEnabled } returns true
every { isCrashLoggingEnabled } returns true
}
@@ -50,6 +55,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel(savedState = null)
val expectedState = DEFAULT_STATE.copy(
showSyncWithBitwarden = false,
showDefaultSaveOptionRow = false,
)
assertEquals(
expectedState,
@@ -66,6 +72,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel(savedState = null)
val expectedState = DEFAULT_STATE.copy(
showSyncWithBitwarden = false,
showDefaultSaveOptionRow = false,
)
assertEquals(
expectedState,
@@ -123,6 +130,24 @@ class SettingsViewModelTest : BaseViewModelTest() {
}
}
@Test
@Suppress("MaxLineLength")
fun `on DefaultSaveOptionUpdated should update SettingsRepository and state`() {
val expectedOption = DefaultSaveOption.BITWARDEN_APP
every { settingsRepository.defaultSaveOption = expectedOption } just runs
val viewModel = createViewModel()
viewModel.trySendAction(SettingsAction.DataClick.DefaultSaveOptionUpdated(expectedOption))
assertEquals(
DEFAULT_STATE.copy(
defaultSaveOption = expectedOption,
),
viewModel.stateFlow.value,
)
verify { settingsRepository.defaultSaveOption = expectedOption }
}
private fun createViewModel(
savedState: SettingsState? = DEFAULT_STATE,
) = SettingsViewModel(
@@ -141,6 +166,7 @@ private val CLOCK = Clock.fixed(
Instant.parse("2024-10-12T12:00:00Z"),
ZoneOffset.UTC,
)
private val DEFAULT_SAVE_OPTION = DefaultSaveOption.NONE
private val DEFAULT_STATE = SettingsState(
appearance = SettingsState.Appearance(
APP_LANGUAGE,
@@ -149,6 +175,8 @@ private val DEFAULT_STATE = SettingsState(
isSubmitCrashLogsEnabled = true,
isUnlockWithBiometricsEnabled = true,
showSyncWithBitwarden = true,
showDefaultSaveOptionRow = true,
defaultSaveOption = DEFAULT_SAVE_OPTION,
dialog = null,
version = R.string.version.asText()
.concat(": ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})".asText()),

View File

@@ -0,0 +1,25 @@
package com.bitwarden.authenticator.ui.platform.util
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.ui.platform.base.util.asText
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class DefaultSaveOptionExtensionsTest {
@Test
fun `displayLabel should map to correct labels`() {
DefaultSaveOption.entries.forEach {
val expected = when (it) {
DefaultSaveOption.BITWARDEN_APP -> R.string.save_to_bitwarden.asText()
DefaultSaveOption.LOCAL -> R.string.save_locally.asText()
DefaultSaveOption.NONE -> R.string.none.asText()
}
assertEquals(
expected,
it.displayLabel,
)
}
}
}