diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreen.kt b/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreen.kt
index f70413dec9..38142a9d49 100644
--- a/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreen.kt
+++ b/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreen.kt
@@ -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 },
diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModel.kt
index b22db36f2a..5f50afd3d9 100644
--- a/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModel.kt
+++ b/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModel.kt
@@ -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()
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2f1da0377f..33174abf26 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -133,7 +133,7 @@
Something went wrong
Please try again
Move to Bitwarden
- Default save options
+ Default save option
Save to Bitwarden
Save locally
None
diff --git a/app/src/test/java/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreenTest.kt b/app/src/test/java/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreenTest.kt
index 279967a8b0..e0196409c7 100644
--- a/app/src/test/java/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreenTest.kt
+++ b/app/src/test/java/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreenTest.kt
@@ -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()
diff --git a/app/src/test/java/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModelTest.kt b/app/src/test/java/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModelTest.kt
index 678d3c807b..6e465aa90a 100644
--- a/app/src/test/java/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModelTest.kt
+++ b/app/src/test/java/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModelTest.kt
@@ -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()