mirror of
https://github.com/bitwarden/android.git
synced 2026-04-29 20:38:41 -05:00
BITAU-181 Allow user to update default save options from settings (#252)
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user