diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt index 7048b320b..af3a0dff8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt @@ -25,6 +25,7 @@ sealed class FlagKey { */ val activeFlags: List> by lazy { listOf( + AuthenticatorSync, EmailVerification, OnboardingFlow, OnboardingCarousel, @@ -32,6 +33,15 @@ sealed class FlagKey { } } + /** + * Data object holding the key for syncing with the Bitwarden Authenticator app. + */ + data object AuthenticatorSync : FlagKey() { + override val keyName: String = "authenticator-sync" + override val defaultValue: Boolean = false + override val isRemotelyConfigured: Boolean = false + } + /** * Data object holding the key for Email Verification feature. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt index 61adc350e..c80de3671 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt @@ -37,6 +37,11 @@ interface SettingsRepository { */ var initialAutofillDialogShown: Boolean + /** + * Whether the user has enabled syncing with the Bitwarden Authenticator app. + */ + var isAuthenticatorSyncEnabled: Boolean + /** * The currently stored last time the vault was synced. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt index 363f65fe8..32a841bd6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt @@ -99,6 +99,9 @@ class SettingsRepositoryImpl( } ?: MutableStateFlow(value = null) + // TODO: this should be backed by disk and should set and clear the sync key (BITAU-103) + override var isAuthenticatorSyncEnabled: Boolean = false + override var isIconLoadingDisabled: Boolean get() = settingsDiskSource.isIconLoadingDisabled ?: false set(value) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt index 9ac839b79..36307ef4c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt @@ -28,13 +28,15 @@ class DebugMenuViewModel @Inject constructor( init { combine( + featureFlagManager.getFeatureFlagFlow(FlagKey.AuthenticatorSync), featureFlagManager.getFeatureFlagFlow(FlagKey.EmailVerification), featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingCarousel), featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow), - ) { (emailVerification, onboardingCarousel, onboardingFlow) -> + ) { (authenticatorSync, emailVerification, onboardingCarousel, onboardingFlow) -> sendAction( DebugMenuAction.Internal.UpdateFeatureFlagMap( mapOf( + FlagKey.AuthenticatorSync to authenticatorSync, FlagKey.EmailVerification to emailVerification, FlagKey.OnboardingCarousel to onboardingCarousel, FlagKey.OnboardingFlow to onboardingFlow, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt index 74c484893..ac291f3e6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt @@ -22,6 +22,7 @@ fun FlagKey.ListItemContent( FlagKey.DummyString, -> Unit + FlagKey.AuthenticatorSync, FlagKey.EmailVerification, FlagKey.OnboardingCarousel, FlagKey.OnboardingFlow, @@ -62,6 +63,7 @@ private fun FlagKey.getDisplayLabel(): String = when (this) { FlagKey.DummyString, -> this.keyName + FlagKey.AuthenticatorSync -> stringResource(R.string.authenticator_sync) FlagKey.EmailVerification -> stringResource(R.string.email_verification) FlagKey.OnboardingCarousel -> stringResource(R.string.onboarding_carousel) FlagKey.OnboardingFlow -> stringResource(R.string.onboarding_flow) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt index 1ee643c58..1df0cc9b7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt @@ -54,6 +54,7 @@ import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.text.BitwardenPolicyWarningText import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithBiometricsSwitch import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithPinSwitch +import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenWideSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager @@ -71,7 +72,7 @@ private const val MINUTES_PER_HOUR = 60 /** * Displays the account security screen. */ -@Suppress("LongMethod", "LongParameterList") +@Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable fun AccountSecurityScreen( @@ -229,6 +230,19 @@ fun AccountSecurityScreen( .padding(horizontal = 16.dp), ) Spacer(Modifier.height(16.dp)) + if (state.shouldShowEnableAuthenticatorSync) { + SyncWithAuthenticatorRow( + isChecked = state.isAuthenticatorSyncChecked, + onCheckedChange = remember(viewModel) { + { + viewModel.trySendAction( + AccountSecurityAction.AuthenticatorSyncToggle(enabled = it), + ) + } + }, + ) + Spacer(Modifier.height(16.dp)) + } BitwardenListHeaderText( label = stringResource(id = R.string.session_timeout), modifier = Modifier @@ -645,6 +659,27 @@ private fun SessionTimeoutActionRow( } } +@Composable +private fun SyncWithAuthenticatorRow( + isChecked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + BitwardenListHeaderText( + label = stringResource(R.string.authenticator_sync), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + BitwardenWideSwitch( + label = stringResource(R.string.allow_bitwarden_authenticator_syncing), + onCheckedChange = onCheckedChange, + isChecked = isChecked, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) +} + @Composable private fun FingerPrintPhraseDialog( fingerprintPhrase: Text, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt index 814e2e321..f95e9c3e6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt @@ -9,7 +9,9 @@ import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult import com.x8bit.bitwarden.data.auth.repository.util.policyInformation import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager +import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager +import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult @@ -45,6 +47,7 @@ class AccountSecurityViewModel @Inject constructor( private val settingsRepository: SettingsRepository, private val environmentRepository: EnvironmentRepository, private val biometricsEncryptionManager: BiometricsEncryptionManager, + private val featureFlagManager: FeatureFlagManager, policyManager: PolicyManager, savedStateHandle: SavedStateHandle, ) : BaseViewModel( @@ -57,6 +60,7 @@ class AccountSecurityViewModel @Inject constructor( AccountSecurityState( dialog = null, fingerprintPhrase = "".asText(), // This will be filled in dynamically + isAuthenticatorSyncChecked = settingsRepository.isAuthenticatorSyncEnabled, isUnlockWithBiometricsEnabled = settingsRepository.isUnlockWithBiometricsEnabled && isBiometricsValid, isUnlockWithPasswordEnabled = authRepository @@ -65,6 +69,9 @@ class AccountSecurityViewModel @Inject constructor( ?.activeAccount ?.hasMasterPassword != false, isUnlockWithPinEnabled = settingsRepository.isUnlockWithPinEnabled, + shouldShowEnableAuthenticatorSync = featureFlagManager.getFeatureFlag( + key = FlagKey.AuthenticatorSync, + ), userId = userId, vaultTimeout = settingsRepository.vaultTimeout, vaultTimeoutAction = settingsRepository.vaultTimeoutAction, @@ -99,6 +106,13 @@ class AccountSecurityViewModel @Inject constructor( .onEach(::sendAction) .launchIn(viewModelScope) + featureFlagManager + .getFeatureFlagFlow(FlagKey.AuthenticatorSync) + .onEach { + trySendAction(AccountSecurityAction.Internal.AuthenticatorSyncFeatureFlagUpdate(it)) + } + .launchIn(viewModelScope) + viewModelScope.launch { trySendAction( AccountSecurityAction.Internal.FingerprintResultReceive( @@ -110,6 +124,7 @@ class AccountSecurityViewModel @Inject constructor( override fun handleAction(action: AccountSecurityAction): Unit = when (action) { AccountSecurityAction.AccountFingerprintPhraseClick -> handleAccountFingerprintPhraseClick() + is AccountSecurityAction.AuthenticatorSyncToggle -> handleAuthenticatorSyncToggle(action) AccountSecurityAction.BackClick -> handleBackClick() AccountSecurityAction.ChangeMasterPasswordClick -> handleChangeMasterPasswordClick() AccountSecurityAction.ConfirmLogoutClick -> handleConfirmLogoutClick() @@ -137,6 +152,13 @@ class AccountSecurityViewModel @Inject constructor( mutableStateFlow.update { it.copy(dialog = AccountSecurityDialog.FingerprintPhrase) } } + private fun handleAuthenticatorSyncToggle( + action: AccountSecurityAction.AuthenticatorSyncToggle, + ) { + settingsRepository.isAuthenticatorSyncEnabled = action.enabled + mutableStateFlow.update { it.copy(isAuthenticatorSyncChecked = action.enabled) } + } + private fun handleBackClick() = sendEvent(AccountSecurityEvent.NavigateBack) private fun handleChangeMasterPasswordClick() { @@ -292,6 +314,10 @@ class AccountSecurityViewModel @Inject constructor( private fun handleInternalAction(action: AccountSecurityAction.Internal) { when (action) { + is AccountSecurityAction.Internal.AuthenticatorSyncFeatureFlagUpdate -> { + handleAuthenticatorSyncFeatureFlagUpdate(action) + } + is AccountSecurityAction.Internal.BiometricsKeyResultReceive -> { handleBiometricsKeyResultReceive(action) } @@ -306,6 +332,16 @@ class AccountSecurityViewModel @Inject constructor( } } + private fun handleAuthenticatorSyncFeatureFlagUpdate( + action: AccountSecurityAction.Internal.AuthenticatorSyncFeatureFlagUpdate, + ) { + mutableStateFlow.update { + it.copy( + shouldShowEnableAuthenticatorSync = action.isEnabled, + ) + } + } + private fun handleBiometricsKeyResultReceive( action: AccountSecurityAction.Internal.BiometricsKeyResultReceive, ) { @@ -377,9 +413,11 @@ class AccountSecurityViewModel @Inject constructor( data class AccountSecurityState( val dialog: AccountSecurityDialog?, val fingerprintPhrase: Text, + val isAuthenticatorSyncChecked: Boolean, val isUnlockWithBiometricsEnabled: Boolean, val isUnlockWithPasswordEnabled: Boolean, val isUnlockWithPinEnabled: Boolean, + val shouldShowEnableAuthenticatorSync: Boolean, val userId: String, val vaultTimeout: VaultTimeout, val vaultTimeoutAction: VaultTimeoutAction, @@ -493,6 +531,13 @@ sealed class AccountSecurityAction { */ data object AccountFingerprintPhraseClick : AccountSecurityAction() + /** + * User clicked the authenticator sync toggle. + */ + data class AuthenticatorSyncToggle( + val enabled: Boolean, + ) : AccountSecurityAction() + /** * User clicked back button. */ @@ -592,6 +637,14 @@ sealed class AccountSecurityAction { * Models actions that can be sent by the view model itself. */ sealed class Internal : AccountSecurityAction() { + + /** + * The feature flag value for authenticator syncing was updated. + */ + data class AuthenticatorSyncFeatureFlagUpdate( + val isEnabled: Boolean, + ) : Internal() + /** * A biometrics key result has been received. */ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index be34cd186..e86ab8c3a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -992,4 +992,6 @@ Do you want to switch to this account? Expired link Please restart registration or try logging in. You may already have an account. Restart registration + Authenticator Sync + Allow Bitwarden Authenticator Syncing diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FlagKeyTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FlagKeyTest.kt new file mode 100644 index 000000000..2ce77ef62 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FlagKeyTest.kt @@ -0,0 +1,13 @@ +package com.x8bit.bitwarden.data.platform.manager + +import com.x8bit.bitwarden.data.platform.manager.model.FlagKey +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test + +class FlagKeyTest { + + @Test + fun `AuthenticatorSync default value should be false`() { + assertFalse(FlagKey.AuthenticatorSync.defaultValue) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt index 1fa18d281..6bc0858e9 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt @@ -1014,6 +1014,18 @@ class SettingsRepositoryTest { settingsRepository.initialAutofillDialogShown = false assertEquals(false, fakeSettingsDiskSource.initialAutofillDialogShown) } + + @Test + fun `isAuthenticatorSyncEnabled should default to false`() { + assertFalse(settingsRepository.isAuthenticatorSyncEnabled) + } + + @Test + fun `isAuthenticatorSyncEnabled should be kept in memory and update accordingly`() = runTest { + assertFalse(settingsRepository.isAuthenticatorSyncEnabled) + settingsRepository.isAuthenticatorSyncEnabled = true + assertTrue(settingsRepository.isAuthenticatorSyncEnabled) + } } private const val USER_ID: String = "userId" diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt index 4556f93f8..2dba0018a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt @@ -76,12 +76,14 @@ class DebugMenuViewModelTest : BaseViewModelTest() { } private val DEFAULT_MAP_VALUE: Map, Any> = mapOf( + FlagKey.AuthenticatorSync to true, FlagKey.EmailVerification to true, FlagKey.OnboardingCarousel to true, FlagKey.OnboardingFlow to true, ) private val UPDATED_MAP_VALUE: Map, Any> = mapOf( + FlagKey.AuthenticatorSync to false, FlagKey.EmailVerification to false, FlagKey.OnboardingCarousel to true, FlagKey.OnboardingFlow to false, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt index dde89bc81..692f647de 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.test.hasClickAction import androidx.compose.ui.test.hasTextExactly import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.isToggleable import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText @@ -74,6 +75,7 @@ class AccountSecurityScreenTest : BaseComposeTest() { private val viewModel = mockk(relaxed = true) { every { eventFlow } returns mutableEventFlow every { stateFlow } returns mutableStateFlow + every { trySendAction(any()) } just runs } @Before @@ -1457,6 +1459,35 @@ class AccountSecurityScreenTest : BaseComposeTest() { composeTestRule.onNodeWithText(rowText).assertDoesNotExist() } + + @Test + fun `sync with Bitwarden authenticator UI should be displayed according to state`() { + val toggleText = "Allow Bitwarden Authenticator Syncing" + composeTestRule.onNodeWithText(toggleText).assertDoesNotExist() + + mutableStateFlow.update { DEFAULT_STATE.copy(shouldShowEnableAuthenticatorSync = true) } + composeTestRule.onNodeWithText(toggleText).performScrollTo().assertIsDisplayed() + composeTestRule.onAllNodesWithText(toggleText).filterToOne(isToggleable()).assertIsOff() + + mutableStateFlow.update { + DEFAULT_STATE.copy( + shouldShowEnableAuthenticatorSync = true, + isAuthenticatorSyncChecked = true, + ) + } + composeTestRule.onNodeWithText(toggleText).assertIsDisplayed() + composeTestRule.onAllNodesWithText(toggleText).filterToOne(isToggleable()).assertIsOn() + } + + @Test + fun `sync with Bitwarden authenticator click should send AuthenticatorSyncToggle action`() { + mutableStateFlow.update { DEFAULT_STATE.copy(shouldShowEnableAuthenticatorSync = true) } + composeTestRule + .onNodeWithText("Allow Bitwarden Authenticator Syncing") + .performScrollTo() + .performClick() + verify { viewModel.trySendAction(AccountSecurityAction.AuthenticatorSyncToggle(true)) } + } } private val CIPHER = mockk() @@ -1464,10 +1495,12 @@ private const val USER_ID: String = "activeUserId" private val DEFAULT_STATE = AccountSecurityState( dialog = null, fingerprintPhrase = "fingerprint-placeholder".asText(), + isAuthenticatorSyncChecked = false, isUnlockWithBiometricsEnabled = false, isUnlockWithPasswordEnabled = true, isUnlockWithPinEnabled = false, userId = USER_ID, + shouldShowEnableAuthenticatorSync = false, vaultTimeout = VaultTimeout.ThirtyMinutes, vaultTimeoutAction = VaultTimeoutAction.LOCK, vaultTimeoutPolicyMinutes = null, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt index e4d228c6c..de803e9a0 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt @@ -8,7 +8,9 @@ import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager +import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager +import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult @@ -24,6 +26,7 @@ import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.components.toggle.UnlockWithPinState +import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.AccountSecurityAction.AuthenticatorSyncToggle import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -32,6 +35,7 @@ import io.mockk.mockk import io.mockk.runs import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json import kotlinx.serialization.json.encodeToJsonElement @@ -49,6 +53,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { } private val vaultRepository: VaultRepository = mockk(relaxed = true) private val settingsRepository: SettingsRepository = mockk { + every { isAuthenticatorSyncEnabled } returns false every { isUnlockWithBiometricsEnabled } returns false every { isUnlockWithPinEnabled } returns false every { vaultTimeout } returns VaultTimeout.ThirtyMinutes @@ -58,12 +63,22 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { private val mutableActivePolicyFlow = bufferedMutableSharedFlow>() private val biometricsEncryptionManager: BiometricsEncryptionManager = mockk { every { createCipherOrNull(DEFAULT_USER_STATE.activeUserId) } returns CIPHER + every { getOrCreateCipher(DEFAULT_USER_STATE.activeUserId) } returns CIPHER + every { + isBiometricIntegrityValid( + userId = DEFAULT_USER_STATE.activeUserId, + cipher = CIPHER, + ) + } returns true } private val policyManager: PolicyManager = mockk { every { getActivePoliciesFlow(type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT) } returns mutableActivePolicyFlow } + private val featureFlagManager: FeatureFlagManager = mockk(relaxed = true) { + every { getFeatureFlag(FlagKey.AuthenticatorSync) } returns false + } @Test fun `initial state should be correct when saved state is set`() { @@ -74,19 +89,9 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { @Test fun `initial state should be correct when saved state is not set`() { - every { settingsRepository.isUnlockWithPinEnabled } returns true - every { - biometricsEncryptionManager.getOrCreateCipher(DEFAULT_USER_STATE.activeUserId) - } returns CIPHER - every { - biometricsEncryptionManager.isBiometricIntegrityValid( - userId = DEFAULT_USER_STATE.activeUserId, - cipher = CIPHER, - ) - } returns true val viewModel = createViewModel(initialState = null) assertEquals( - DEFAULT_STATE.copy(isUnlockWithPinEnabled = true), + DEFAULT_STATE, viewModel.stateFlow.value, ) verify { @@ -128,6 +133,20 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `on AuthenticatorSyncToggle should update SettingsRepository and isAuthenticatorSyncChecked`() = + runTest { + val viewModel = createViewModel() + every { settingsRepository.isAuthenticatorSyncEnabled = true } just runs + viewModel.stateFlow.test { + assertEquals(DEFAULT_STATE, awaitItem()) + viewModel.trySendAction(AuthenticatorSyncToggle(enabled = true)) + assertEquals(DEFAULT_STATE.copy(isAuthenticatorSyncChecked = true), awaitItem()) + } + verify { settingsRepository.isAuthenticatorSyncEnabled = true } + } + @Test fun `on FingerprintResultReceive should update the fingerprint phrase`() = runTest { val fingerprint = "fingerprint" @@ -597,10 +616,46 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `when featureFlagManger returns true for AuthenticatorSync, should show authenticator sync UI`() { + val vm = createViewModel( + initialState = null, + featureFlagManager = mockk { + every { getFeatureFlag(FlagKey.AuthenticatorSync) } returns true + every { getFeatureFlagFlow(FlagKey.AuthenticatorSync) } returns emptyFlow() + }, + ) + assertEquals( + DEFAULT_STATE.copy(shouldShowEnableAuthenticatorSync = true), + vm.stateFlow.value, + ) + } + + @Test + fun `when featureFlagManger updates value AuthenticatorSync, should update UI`() = runTest { + val featureFlagFlow = MutableStateFlow(false) + val vm = createViewModel( + initialState = null, + featureFlagManager = mockk { + every { getFeatureFlag(FlagKey.AuthenticatorSync) } returns false + every { getFeatureFlagFlow(FlagKey.AuthenticatorSync) } returns featureFlagFlow + }, + ) + vm.stateFlow.test { + assertEquals(DEFAULT_STATE, awaitItem()) + featureFlagFlow.value = true + assertEquals(DEFAULT_STATE.copy(shouldShowEnableAuthenticatorSync = true), awaitItem()) + featureFlagFlow.value = false + assertEquals(DEFAULT_STATE, awaitItem()) + } + } + @Suppress("LongParameterList") private fun createViewModel( initialState: AccountSecurityState? = DEFAULT_STATE, authRepository: AuthRepository = this.authRepository, + featureFlagManager: FeatureFlagManager = this.featureFlagManager, vaultRepository: VaultRepository = this.vaultRepository, environmentRepository: EnvironmentRepository = this.fakeEnvironmentRepository, settingsRepository: SettingsRepository = this.settingsRepository, @@ -608,6 +663,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { policyManager: PolicyManager = this.policyManager, ): AccountSecurityViewModel = AccountSecurityViewModel( authRepository = authRepository, + featureFlagManager = featureFlagManager, vaultRepository = vaultRepository, settingsRepository = settingsRepository, environmentRepository = environmentRepository, @@ -648,6 +704,7 @@ private val DEFAULT_USER_STATE = UserState( private val DEFAULT_STATE: AccountSecurityState = AccountSecurityState( dialog = null, fingerprintPhrase = FINGERPRINT.asText(), + isAuthenticatorSyncChecked = false, isUnlockWithBiometricsEnabled = false, isUnlockWithPasswordEnabled = true, isUnlockWithPinEnabled = false, @@ -656,4 +713,5 @@ private val DEFAULT_STATE: AccountSecurityState = AccountSecurityState( vaultTimeoutAction = VaultTimeoutAction.LOCK, vaultTimeoutPolicyMinutes = null, vaultTimeoutPolicyAction = null, + shouldShowEnableAuthenticatorSync = false, )