Skip to content

Commit

Permalink
BITAU-189 BITAU-188 Only show default save option row when sync is en… (
Browse files Browse the repository at this point in the history
  • Loading branch information
ahaisting-livefront authored Oct 30, 2024
1 parent c0abc1d commit aa42e0a
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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 -> {
Expand All @@ -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,
)
}
}

Expand Down Expand Up @@ -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,
Expand All @@ -293,7 +328,7 @@ class SettingsViewModel @Inject constructor(
copyrightInfo = copyrightInfo,
defaultSaveOption = defaultSaveOption,
showSyncWithBitwarden = shouldShowSyncWithBitwarden,
showDefaultSaveOptionRow = shouldShowSyncWithBitwarden,
showDefaultSaveOptionRow = shouldShowDefaultSaveOption,
)
}
}
Expand Down Expand Up @@ -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()
}
}
2 changes: 1 addition & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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`() {
Expand Down Expand Up @@ -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`() {
Expand All @@ -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(
Expand All @@ -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()
Expand Down

0 comments on commit aa42e0a

Please sign in to comment.