BITAU-189 BITAU-188 Only show default save option row when sync is en… (#261)

This commit is contained in:
Andrew Haisting
2024-10-30 10:32:57 -05:00
committed by GitHub
parent c0abc1d647
commit aa42e0ad18
5 changed files with 101 additions and 12 deletions
@@ -373,7 +373,7 @@ private fun DefaultSaveOptionSelectionRow(
var shouldShowDefaultSaveOptionDialog by remember { mutableStateOf(false) }
BitwardenTextRow(
text = stringResource(id = R.string.default_save_options),
text = stringResource(id = R.string.default_save_option),
onClick = { shouldShowDefaultSaveOptionDialog = true },
modifier = modifier,
withDivider = true,
@@ -388,7 +388,7 @@ private fun DefaultSaveOptionSelectionRow(
var dialogSelection by remember { mutableStateOf(currentSelection) }
if (shouldShowDefaultSaveOptionDialog) {
BitwardenSelectionDialog(
title = stringResource(id = R.string.default_save_options),
title = stringResource(id = R.string.default_save_option),
subtitle = stringResource(id = R.string.default_save_options_subtitle),
dismissLabel = stringResource(id = R.string.confirm),
onDismissRequest = { shouldShowDefaultSaveOptionDialog = false },
@@ -7,6 +7,9 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.authenticator.BuildConfig
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState
import com.bitwarden.authenticator.data.authenticator.repository.util.isSyncWithBitwardenEnabled
import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
import com.bitwarden.authenticator.data.platform.manager.model.LocalFeatureFlag
@@ -22,6 +25,9 @@ import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.Defau
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@@ -34,11 +40,12 @@ private const val KEY_STATE = "state"
/**
* View model for the settings screen.
*/
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LongParameterList")
@HiltViewModel
class SettingsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
clock: Clock,
private val authenticatorRepository: AuthenticatorRepository,
private val authenticatorBridgeManager: AuthenticatorBridgeManager,
private val settingsRepository: SettingsRepository,
private val clipboardManager: BitwardenClipboardManager,
@@ -55,8 +62,18 @@ class SettingsViewModel @Inject constructor(
featureFlagManager.getFeatureFlag(LocalFeatureFlag.PasswordManagerSync),
accountSyncState = authenticatorBridgeManager.accountSyncStateFlow.value,
defaultSaveOption = settingsRepository.defaultSaveOption,
sharedAccountsState = authenticatorRepository.sharedCodesStateFlow.value,
),
) {
init {
authenticatorRepository
.sharedCodesStateFlow
.map { SettingsAction.Internal.SharedAccountsStateUpdated(it) }
.onEach(::handleAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: SettingsAction) {
when (action) {
is SettingsAction.SecurityClick -> {
@@ -82,6 +99,20 @@ class SettingsViewModel @Inject constructor(
is SettingsAction.Internal.BiometricsKeyResultReceive -> {
handleBiometricsKeyResultReceive(action)
}
is SettingsAction.Internal.SharedAccountsStateUpdated -> {
handleSharedAccountsStateUpdated(action)
}
}
}
private fun handleSharedAccountsStateUpdated(
action: SettingsAction.Internal.SharedAccountsStateUpdated,
) {
mutableStateFlow.update {
it.copy(
showDefaultSaveOptionRow = action.state.isSyncWithBitwardenEnabled,
)
}
}
@@ -273,12 +304,16 @@ class SettingsViewModel @Inject constructor(
isSubmitCrashLogsEnabled: Boolean,
accountSyncState: AccountSyncState,
isSyncWithBitwardenFeatureEnabled: Boolean,
sharedAccountsState: SharedVerificationCodesState,
): SettingsState {
val currentYear = Year.now(clock)
val copyrightInfo = "© Bitwarden Inc. 2015-$currentYear".asText()
// Show sync with Bitwarden row if feature is enabled and the OS is supported:
val shouldShowSyncWithBitwarden = isSyncWithBitwardenFeatureEnabled &&
accountSyncState != AccountSyncState.OsVersionNotSupported
// Show default save options only if the user had enabled sync with Bitwarden:
// (They can enable it via the "Sync with Bitwarden" row.
val shouldShowDefaultSaveOption = sharedAccountsState.isSyncWithBitwardenEnabled
return SettingsState(
appearance = SettingsState.Appearance(
language = appLanguage,
@@ -293,7 +328,7 @@ class SettingsViewModel @Inject constructor(
copyrightInfo = copyrightInfo,
defaultSaveOption = defaultSaveOption,
showSyncWithBitwarden = shouldShowSyncWithBitwarden,
showDefaultSaveOptionRow = shouldShowSyncWithBitwarden,
showDefaultSaveOptionRow = shouldShowDefaultSaveOption,
)
}
}
@@ -511,5 +546,12 @@ sealed class SettingsAction(
* Indicates the biometrics key validation results has been received.
*/
data class BiometricsKeyResultReceive(val result: BiometricsKeyResult) : SettingsAction()
/**
* Indicates that shared account state was updated.
*/
data class SharedAccountsStateUpdated(
val state: SharedVerificationCodesState,
) : SettingsAction()
}
}
+1 -1
View File
@@ -133,7 +133,7 @@
<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="default_save_option">Default save option</string>
<string name="save_to_bitwarden">Save to Bitwarden</string>
<string name="save_locally">Save locally</string>
<string name="none">None</string>
@@ -119,32 +119,32 @@ class SettingsScreenTest : BaseComposeTest() {
}
@Test
fun `Default Save Options row should be hidden when showDefaultSaveOptionRow is false`() {
fun `Default Save Option row should be hidden when showDefaultSaveOptionRow is false`() {
mutableStateFlow.value = DEFAULT_STATE
composeTestRule.onNodeWithText("Default save options").assertExists()
composeTestRule.onNodeWithText("Default save option").assertExists()
mutableStateFlow.update {
it.copy(
showDefaultSaveOptionRow = false,
)
}
composeTestRule.onNodeWithText("Default save options").assertDoesNotExist()
composeTestRule.onNodeWithText("Default save option").assertDoesNotExist()
}
@Test
@Suppress("MaxLineLength")
fun `Default Save Options dialog should send DefaultSaveOptionUpdated when confirm is clicked`() =
fun `Default Save Option dialog should send DefaultSaveOptionUpdated when confirm is clicked`() =
runTest {
val expectedSaveOption = DefaultSaveOption.BITWARDEN_APP
mutableStateFlow.value = DEFAULT_STATE
composeTestRule
.onNodeWithText("Default save options")
.onNodeWithText("Default save option")
.performScrollTo()
.performClick()
// Make sure the dialog is showing:
composeTestRule
.onAllNodesWithText("Default save options")
.onAllNodesWithText("Default save option")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
@@ -4,6 +4,9 @@ import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.authenticator.BuildConfig
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState
import com.bitwarden.authenticator.data.authenticator.repository.util.isSyncWithBitwardenEnabled
import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
import com.bitwarden.authenticator.data.platform.manager.model.LocalFeatureFlag
@@ -19,11 +22,15 @@ import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
@@ -34,6 +41,11 @@ class SettingsViewModelTest : BaseViewModelTest() {
private val authenticatorBridgeManager: AuthenticatorBridgeManager = mockk {
every { accountSyncStateFlow } returns MutableStateFlow(AccountSyncState.Loading)
}
private val mutableSharedCodesFlow = MutableStateFlow(MOCK_SHARED_CODES_STATE)
private val authenticatorRepository: AuthenticatorRepository = mockk {
every { sharedCodesStateFlow } returns mutableSharedCodesFlow
}
private val settingsRepository: SettingsRepository = mockk {
every { appLanguage } returns APP_LANGUAGE
every { appTheme } returns APP_THEME
@@ -46,6 +58,16 @@ class SettingsViewModelTest : BaseViewModelTest() {
every { getFeatureFlag(LocalFeatureFlag.PasswordManagerSync) } returns true
}
@BeforeEach
fun setup() {
mockkStatic(SharedVerificationCodesState::isSyncWithBitwardenEnabled)
every { MOCK_SHARED_CODES_STATE.isSyncWithBitwardenEnabled } returns false
}
fun teardown() {
unmockkStatic(SharedVerificationCodesState::isSyncWithBitwardenEnabled)
}
@Test
@Suppress("MaxLineLength")
fun `initialState should be correct when saved state is null and password manager feature flag is off`() {
@@ -130,6 +152,29 @@ class SettingsViewModelTest : BaseViewModelTest() {
}
}
@Test
@Suppress("MaxLineLength")
fun `Default save option row should only show when shared codes state shows syncing as enabled`() =
runTest {
val viewModel = createViewModel()
val enabledState: SharedVerificationCodesState = mockk {
every { isSyncWithBitwardenEnabled } returns true
}
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE,
awaitItem(),
)
mutableSharedCodesFlow.update { enabledState }
assertEquals(
DEFAULT_STATE.copy(
showDefaultSaveOptionRow = true,
),
awaitItem(),
)
}
}
@Test
@Suppress("MaxLineLength")
fun `on DefaultSaveOptionUpdated should update SettingsRepository and state`() {
@@ -154,12 +199,14 @@ class SettingsViewModelTest : BaseViewModelTest() {
savedStateHandle = SavedStateHandle().apply { this["state"] = savedState },
clock = CLOCK,
authenticatorBridgeManager = authenticatorBridgeManager,
authenticatorRepository = authenticatorRepository,
settingsRepository = settingsRepository,
clipboardManager = clipboardManager,
featureFlagManager = featureFlagManager,
)
}
private val MOCK_SHARED_CODES_STATE: SharedVerificationCodesState = mockk()
private val APP_LANGUAGE = AppLanguage.ENGLISH
private val APP_THEME = AppTheme.DEFAULT
private val CLOCK = Clock.fixed(
@@ -175,7 +222,7 @@ private val DEFAULT_STATE = SettingsState(
isSubmitCrashLogsEnabled = true,
isUnlockWithBiometricsEnabled = true,
showSyncWithBitwarden = true,
showDefaultSaveOptionRow = true,
showDefaultSaveOptionRow = false,
defaultSaveOption = DEFAULT_SAVE_OPTION,
dialog = null,
version = R.string.version.asText()