mirror of
https://github.com/bitwarden/android.git
synced 2024-11-25 19:06:05 +03:00
BITAU-107 Add Feature Flag and UI for Authenticator Syncing (#3847)
This commit is contained in:
parent
69ca7649e2
commit
b1ecf125d1
13 changed files with 243 additions and 13 deletions
|
@ -25,6 +25,7 @@ sealed class FlagKey<out T : Any> {
|
||||||
*/
|
*/
|
||||||
val activeFlags: List<FlagKey<*>> by lazy {
|
val activeFlags: List<FlagKey<*>> by lazy {
|
||||||
listOf(
|
listOf(
|
||||||
|
AuthenticatorSync,
|
||||||
EmailVerification,
|
EmailVerification,
|
||||||
OnboardingFlow,
|
OnboardingFlow,
|
||||||
OnboardingCarousel,
|
OnboardingCarousel,
|
||||||
|
@ -32,6 +33,15 @@ sealed class FlagKey<out T : Any> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data object holding the key for syncing with the Bitwarden Authenticator app.
|
||||||
|
*/
|
||||||
|
data object AuthenticatorSync : FlagKey<Boolean>() {
|
||||||
|
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.
|
* Data object holding the key for Email Verification feature.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -37,6 +37,11 @@ interface SettingsRepository {
|
||||||
*/
|
*/
|
||||||
var initialAutofillDialogShown: Boolean
|
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.
|
* The currently stored last time the vault was synced.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -99,6 +99,9 @@ class SettingsRepositoryImpl(
|
||||||
}
|
}
|
||||||
?: MutableStateFlow(value = null)
|
?: 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
|
override var isIconLoadingDisabled: Boolean
|
||||||
get() = settingsDiskSource.isIconLoadingDisabled ?: false
|
get() = settingsDiskSource.isIconLoadingDisabled ?: false
|
||||||
set(value) {
|
set(value) {
|
||||||
|
|
|
@ -28,13 +28,15 @@ class DebugMenuViewModel @Inject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
combine(
|
combine(
|
||||||
|
featureFlagManager.getFeatureFlagFlow(FlagKey.AuthenticatorSync),
|
||||||
featureFlagManager.getFeatureFlagFlow(FlagKey.EmailVerification),
|
featureFlagManager.getFeatureFlagFlow(FlagKey.EmailVerification),
|
||||||
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingCarousel),
|
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingCarousel),
|
||||||
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
|
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
|
||||||
) { (emailVerification, onboardingCarousel, onboardingFlow) ->
|
) { (authenticatorSync, emailVerification, onboardingCarousel, onboardingFlow) ->
|
||||||
sendAction(
|
sendAction(
|
||||||
DebugMenuAction.Internal.UpdateFeatureFlagMap(
|
DebugMenuAction.Internal.UpdateFeatureFlagMap(
|
||||||
mapOf(
|
mapOf(
|
||||||
|
FlagKey.AuthenticatorSync to authenticatorSync,
|
||||||
FlagKey.EmailVerification to emailVerification,
|
FlagKey.EmailVerification to emailVerification,
|
||||||
FlagKey.OnboardingCarousel to onboardingCarousel,
|
FlagKey.OnboardingCarousel to onboardingCarousel,
|
||||||
FlagKey.OnboardingFlow to onboardingFlow,
|
FlagKey.OnboardingFlow to onboardingFlow,
|
||||||
|
|
|
@ -22,6 +22,7 @@ fun <T : Any> FlagKey<T>.ListItemContent(
|
||||||
FlagKey.DummyString,
|
FlagKey.DummyString,
|
||||||
-> Unit
|
-> Unit
|
||||||
|
|
||||||
|
FlagKey.AuthenticatorSync,
|
||||||
FlagKey.EmailVerification,
|
FlagKey.EmailVerification,
|
||||||
FlagKey.OnboardingCarousel,
|
FlagKey.OnboardingCarousel,
|
||||||
FlagKey.OnboardingFlow,
|
FlagKey.OnboardingFlow,
|
||||||
|
@ -62,6 +63,7 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
|
||||||
FlagKey.DummyString,
|
FlagKey.DummyString,
|
||||||
-> this.keyName
|
-> this.keyName
|
||||||
|
|
||||||
|
FlagKey.AuthenticatorSync -> stringResource(R.string.authenticator_sync)
|
||||||
FlagKey.EmailVerification -> stringResource(R.string.email_verification)
|
FlagKey.EmailVerification -> stringResource(R.string.email_verification)
|
||||||
FlagKey.OnboardingCarousel -> stringResource(R.string.onboarding_carousel)
|
FlagKey.OnboardingCarousel -> stringResource(R.string.onboarding_carousel)
|
||||||
FlagKey.OnboardingFlow -> stringResource(R.string.onboarding_flow)
|
FlagKey.OnboardingFlow -> stringResource(R.string.onboarding_flow)
|
||||||
|
|
|
@ -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.text.BitwardenPolicyWarningText
|
||||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithBiometricsSwitch
|
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.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.components.util.rememberVectorPainter
|
||||||
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
|
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
|
||||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||||
|
@ -71,7 +72,7 @@ private const val MINUTES_PER_HOUR = 60
|
||||||
/**
|
/**
|
||||||
* Displays the account security screen.
|
* Displays the account security screen.
|
||||||
*/
|
*/
|
||||||
@Suppress("LongMethod", "LongParameterList")
|
@Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AccountSecurityScreen(
|
fun AccountSecurityScreen(
|
||||||
|
@ -229,6 +230,19 @@ fun AccountSecurityScreen(
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(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(
|
BitwardenListHeaderText(
|
||||||
label = stringResource(id = R.string.session_timeout),
|
label = stringResource(id = R.string.session_timeout),
|
||||||
modifier = Modifier
|
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
|
@Composable
|
||||||
private fun FingerPrintPhraseDialog(
|
private fun FingerPrintPhraseDialog(
|
||||||
fingerprintPhrase: Text,
|
fingerprintPhrase: Text,
|
||||||
|
|
|
@ -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.model.UserFingerprintResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
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.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.EnvironmentRepository
|
||||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
||||||
|
@ -45,6 +47,7 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository,
|
||||||
private val environmentRepository: EnvironmentRepository,
|
private val environmentRepository: EnvironmentRepository,
|
||||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||||
|
private val featureFlagManager: FeatureFlagManager,
|
||||||
policyManager: PolicyManager,
|
policyManager: PolicyManager,
|
||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
) : BaseViewModel<AccountSecurityState, AccountSecurityEvent, AccountSecurityAction>(
|
) : BaseViewModel<AccountSecurityState, AccountSecurityEvent, AccountSecurityAction>(
|
||||||
|
@ -57,6 +60,7 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
AccountSecurityState(
|
AccountSecurityState(
|
||||||
dialog = null,
|
dialog = null,
|
||||||
fingerprintPhrase = "".asText(), // This will be filled in dynamically
|
fingerprintPhrase = "".asText(), // This will be filled in dynamically
|
||||||
|
isAuthenticatorSyncChecked = settingsRepository.isAuthenticatorSyncEnabled,
|
||||||
isUnlockWithBiometricsEnabled = settingsRepository.isUnlockWithBiometricsEnabled &&
|
isUnlockWithBiometricsEnabled = settingsRepository.isUnlockWithBiometricsEnabled &&
|
||||||
isBiometricsValid,
|
isBiometricsValid,
|
||||||
isUnlockWithPasswordEnabled = authRepository
|
isUnlockWithPasswordEnabled = authRepository
|
||||||
|
@ -65,6 +69,9 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
?.activeAccount
|
?.activeAccount
|
||||||
?.hasMasterPassword != false,
|
?.hasMasterPassword != false,
|
||||||
isUnlockWithPinEnabled = settingsRepository.isUnlockWithPinEnabled,
|
isUnlockWithPinEnabled = settingsRepository.isUnlockWithPinEnabled,
|
||||||
|
shouldShowEnableAuthenticatorSync = featureFlagManager.getFeatureFlag(
|
||||||
|
key = FlagKey.AuthenticatorSync,
|
||||||
|
),
|
||||||
userId = userId,
|
userId = userId,
|
||||||
vaultTimeout = settingsRepository.vaultTimeout,
|
vaultTimeout = settingsRepository.vaultTimeout,
|
||||||
vaultTimeoutAction = settingsRepository.vaultTimeoutAction,
|
vaultTimeoutAction = settingsRepository.vaultTimeoutAction,
|
||||||
|
@ -99,6 +106,13 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
.onEach(::sendAction)
|
.onEach(::sendAction)
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
featureFlagManager
|
||||||
|
.getFeatureFlagFlow(FlagKey.AuthenticatorSync)
|
||||||
|
.onEach {
|
||||||
|
trySendAction(AccountSecurityAction.Internal.AuthenticatorSyncFeatureFlagUpdate(it))
|
||||||
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
trySendAction(
|
trySendAction(
|
||||||
AccountSecurityAction.Internal.FingerprintResultReceive(
|
AccountSecurityAction.Internal.FingerprintResultReceive(
|
||||||
|
@ -110,6 +124,7 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
|
|
||||||
override fun handleAction(action: AccountSecurityAction): Unit = when (action) {
|
override fun handleAction(action: AccountSecurityAction): Unit = when (action) {
|
||||||
AccountSecurityAction.AccountFingerprintPhraseClick -> handleAccountFingerprintPhraseClick()
|
AccountSecurityAction.AccountFingerprintPhraseClick -> handleAccountFingerprintPhraseClick()
|
||||||
|
is AccountSecurityAction.AuthenticatorSyncToggle -> handleAuthenticatorSyncToggle(action)
|
||||||
AccountSecurityAction.BackClick -> handleBackClick()
|
AccountSecurityAction.BackClick -> handleBackClick()
|
||||||
AccountSecurityAction.ChangeMasterPasswordClick -> handleChangeMasterPasswordClick()
|
AccountSecurityAction.ChangeMasterPasswordClick -> handleChangeMasterPasswordClick()
|
||||||
AccountSecurityAction.ConfirmLogoutClick -> handleConfirmLogoutClick()
|
AccountSecurityAction.ConfirmLogoutClick -> handleConfirmLogoutClick()
|
||||||
|
@ -137,6 +152,13 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
mutableStateFlow.update { it.copy(dialog = AccountSecurityDialog.FingerprintPhrase) }
|
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 handleBackClick() = sendEvent(AccountSecurityEvent.NavigateBack)
|
||||||
|
|
||||||
private fun handleChangeMasterPasswordClick() {
|
private fun handleChangeMasterPasswordClick() {
|
||||||
|
@ -292,6 +314,10 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun handleInternalAction(action: AccountSecurityAction.Internal) {
|
private fun handleInternalAction(action: AccountSecurityAction.Internal) {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
is AccountSecurityAction.Internal.AuthenticatorSyncFeatureFlagUpdate -> {
|
||||||
|
handleAuthenticatorSyncFeatureFlagUpdate(action)
|
||||||
|
}
|
||||||
|
|
||||||
is AccountSecurityAction.Internal.BiometricsKeyResultReceive -> {
|
is AccountSecurityAction.Internal.BiometricsKeyResultReceive -> {
|
||||||
handleBiometricsKeyResultReceive(action)
|
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(
|
private fun handleBiometricsKeyResultReceive(
|
||||||
action: AccountSecurityAction.Internal.BiometricsKeyResultReceive,
|
action: AccountSecurityAction.Internal.BiometricsKeyResultReceive,
|
||||||
) {
|
) {
|
||||||
|
@ -377,9 +413,11 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
data class AccountSecurityState(
|
data class AccountSecurityState(
|
||||||
val dialog: AccountSecurityDialog?,
|
val dialog: AccountSecurityDialog?,
|
||||||
val fingerprintPhrase: Text,
|
val fingerprintPhrase: Text,
|
||||||
|
val isAuthenticatorSyncChecked: Boolean,
|
||||||
val isUnlockWithBiometricsEnabled: Boolean,
|
val isUnlockWithBiometricsEnabled: Boolean,
|
||||||
val isUnlockWithPasswordEnabled: Boolean,
|
val isUnlockWithPasswordEnabled: Boolean,
|
||||||
val isUnlockWithPinEnabled: Boolean,
|
val isUnlockWithPinEnabled: Boolean,
|
||||||
|
val shouldShowEnableAuthenticatorSync: Boolean,
|
||||||
val userId: String,
|
val userId: String,
|
||||||
val vaultTimeout: VaultTimeout,
|
val vaultTimeout: VaultTimeout,
|
||||||
val vaultTimeoutAction: VaultTimeoutAction,
|
val vaultTimeoutAction: VaultTimeoutAction,
|
||||||
|
@ -493,6 +531,13 @@ sealed class AccountSecurityAction {
|
||||||
*/
|
*/
|
||||||
data object AccountFingerprintPhraseClick : AccountSecurityAction()
|
data object AccountFingerprintPhraseClick : AccountSecurityAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User clicked the authenticator sync toggle.
|
||||||
|
*/
|
||||||
|
data class AuthenticatorSyncToggle(
|
||||||
|
val enabled: Boolean,
|
||||||
|
) : AccountSecurityAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User clicked back button.
|
* User clicked back button.
|
||||||
*/
|
*/
|
||||||
|
@ -592,6 +637,14 @@ sealed class AccountSecurityAction {
|
||||||
* Models actions that can be sent by the view model itself.
|
* Models actions that can be sent by the view model itself.
|
||||||
*/
|
*/
|
||||||
sealed class Internal : AccountSecurityAction() {
|
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.
|
* A biometrics key result has been received.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -992,4 +992,6 @@ Do you want to switch to this account?</string>
|
||||||
<string name="expired_link">Expired link</string>
|
<string name="expired_link">Expired link</string>
|
||||||
<string name="please_restart_registration_or_try_logging_in">Please restart registration or try logging in. You may already have an account.</string>
|
<string name="please_restart_registration_or_try_logging_in">Please restart registration or try logging in. You may already have an account.</string>
|
||||||
<string name="restart_registration">Restart registration</string>
|
<string name="restart_registration">Restart registration</string>
|
||||||
|
<string name="authenticator_sync">Authenticator Sync</string>
|
||||||
|
<string name="allow_bitwarden_authenticator_syncing">Allow Bitwarden Authenticator Syncing</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1014,6 +1014,18 @@ class SettingsRepositoryTest {
|
||||||
settingsRepository.initialAutofillDialogShown = false
|
settingsRepository.initialAutofillDialogShown = false
|
||||||
assertEquals(false, fakeSettingsDiskSource.initialAutofillDialogShown)
|
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"
|
private const val USER_ID: String = "userId"
|
||||||
|
|
|
@ -76,12 +76,14 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private val DEFAULT_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
|
private val DEFAULT_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
|
||||||
|
FlagKey.AuthenticatorSync to true,
|
||||||
FlagKey.EmailVerification to true,
|
FlagKey.EmailVerification to true,
|
||||||
FlagKey.OnboardingCarousel to true,
|
FlagKey.OnboardingCarousel to true,
|
||||||
FlagKey.OnboardingFlow to true,
|
FlagKey.OnboardingFlow to true,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val UPDATED_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
|
private val UPDATED_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
|
||||||
|
FlagKey.AuthenticatorSync to false,
|
||||||
FlagKey.EmailVerification to false,
|
FlagKey.EmailVerification to false,
|
||||||
FlagKey.OnboardingCarousel to true,
|
FlagKey.OnboardingCarousel to true,
|
||||||
FlagKey.OnboardingFlow to false,
|
FlagKey.OnboardingFlow to false,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import androidx.compose.ui.test.hasClickAction
|
||||||
import androidx.compose.ui.test.hasTextExactly
|
import androidx.compose.ui.test.hasTextExactly
|
||||||
import androidx.compose.ui.test.isDialog
|
import androidx.compose.ui.test.isDialog
|
||||||
import androidx.compose.ui.test.isDisplayed
|
import androidx.compose.ui.test.isDisplayed
|
||||||
|
import androidx.compose.ui.test.isToggleable
|
||||||
import androidx.compose.ui.test.onAllNodesWithText
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
@ -74,6 +75,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
||||||
private val viewModel = mockk<AccountSecurityViewModel>(relaxed = true) {
|
private val viewModel = mockk<AccountSecurityViewModel>(relaxed = true) {
|
||||||
every { eventFlow } returns mutableEventFlow
|
every { eventFlow } returns mutableEventFlow
|
||||||
every { stateFlow } returns mutableStateFlow
|
every { stateFlow } returns mutableStateFlow
|
||||||
|
every { trySendAction(any()) } just runs
|
||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
@ -1457,6 +1459,35 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(rowText).assertDoesNotExist()
|
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<Cipher>()
|
private val CIPHER = mockk<Cipher>()
|
||||||
|
@ -1464,10 +1495,12 @@ private const val USER_ID: String = "activeUserId"
|
||||||
private val DEFAULT_STATE = AccountSecurityState(
|
private val DEFAULT_STATE = AccountSecurityState(
|
||||||
dialog = null,
|
dialog = null,
|
||||||
fingerprintPhrase = "fingerprint-placeholder".asText(),
|
fingerprintPhrase = "fingerprint-placeholder".asText(),
|
||||||
|
isAuthenticatorSyncChecked = false,
|
||||||
isUnlockWithBiometricsEnabled = false,
|
isUnlockWithBiometricsEnabled = false,
|
||||||
isUnlockWithPasswordEnabled = true,
|
isUnlockWithPasswordEnabled = true,
|
||||||
isUnlockWithPinEnabled = false,
|
isUnlockWithPinEnabled = false,
|
||||||
userId = USER_ID,
|
userId = USER_ID,
|
||||||
|
shouldShowEnableAuthenticatorSync = false,
|
||||||
vaultTimeout = VaultTimeout.ThirtyMinutes,
|
vaultTimeout = VaultTimeout.ThirtyMinutes,
|
||||||
vaultTimeoutAction = VaultTimeoutAction.LOCK,
|
vaultTimeoutAction = VaultTimeoutAction.LOCK,
|
||||||
vaultTimeoutPolicyMinutes = null,
|
vaultTimeoutPolicyMinutes = null,
|
||||||
|
|
|
@ -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.UserFingerprintResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
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.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.EnvironmentRepository
|
||||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
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.BaseViewModelTest
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
import com.x8bit.bitwarden.ui.platform.components.toggle.UnlockWithPinState
|
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.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
@ -32,6 +35,7 @@ import io.mockk.mockk
|
||||||
import io.mockk.runs
|
import io.mockk.runs
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.encodeToJsonElement
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
|
@ -49,6 +53,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
private val vaultRepository: VaultRepository = mockk(relaxed = true)
|
private val vaultRepository: VaultRepository = mockk(relaxed = true)
|
||||||
private val settingsRepository: SettingsRepository = mockk {
|
private val settingsRepository: SettingsRepository = mockk {
|
||||||
|
every { isAuthenticatorSyncEnabled } returns false
|
||||||
every { isUnlockWithBiometricsEnabled } returns false
|
every { isUnlockWithBiometricsEnabled } returns false
|
||||||
every { isUnlockWithPinEnabled } returns false
|
every { isUnlockWithPinEnabled } returns false
|
||||||
every { vaultTimeout } returns VaultTimeout.ThirtyMinutes
|
every { vaultTimeout } returns VaultTimeout.ThirtyMinutes
|
||||||
|
@ -58,12 +63,22 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
private val mutableActivePolicyFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Policy>>()
|
private val mutableActivePolicyFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Policy>>()
|
||||||
private val biometricsEncryptionManager: BiometricsEncryptionManager = mockk {
|
private val biometricsEncryptionManager: BiometricsEncryptionManager = mockk {
|
||||||
every { createCipherOrNull(DEFAULT_USER_STATE.activeUserId) } returns CIPHER
|
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 {
|
private val policyManager: PolicyManager = mockk {
|
||||||
every {
|
every {
|
||||||
getActivePoliciesFlow(type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT)
|
getActivePoliciesFlow(type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT)
|
||||||
} returns mutableActivePolicyFlow
|
} returns mutableActivePolicyFlow
|
||||||
}
|
}
|
||||||
|
private val featureFlagManager: FeatureFlagManager = mockk(relaxed = true) {
|
||||||
|
every { getFeatureFlag(FlagKey.AuthenticatorSync) } returns false
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial state should be correct when saved state is set`() {
|
fun `initial state should be correct when saved state is set`() {
|
||||||
|
@ -74,19 +89,9 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial state should be correct when saved state is not set`() {
|
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)
|
val viewModel = createViewModel(initialState = null)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
DEFAULT_STATE.copy(isUnlockWithPinEnabled = true),
|
DEFAULT_STATE,
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
verify {
|
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
|
@Test
|
||||||
fun `on FingerprintResultReceive should update the fingerprint phrase`() = runTest {
|
fun `on FingerprintResultReceive should update the fingerprint phrase`() = runTest {
|
||||||
val fingerprint = "fingerprint"
|
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")
|
@Suppress("LongParameterList")
|
||||||
private fun createViewModel(
|
private fun createViewModel(
|
||||||
initialState: AccountSecurityState? = DEFAULT_STATE,
|
initialState: AccountSecurityState? = DEFAULT_STATE,
|
||||||
authRepository: AuthRepository = this.authRepository,
|
authRepository: AuthRepository = this.authRepository,
|
||||||
|
featureFlagManager: FeatureFlagManager = this.featureFlagManager,
|
||||||
vaultRepository: VaultRepository = this.vaultRepository,
|
vaultRepository: VaultRepository = this.vaultRepository,
|
||||||
environmentRepository: EnvironmentRepository = this.fakeEnvironmentRepository,
|
environmentRepository: EnvironmentRepository = this.fakeEnvironmentRepository,
|
||||||
settingsRepository: SettingsRepository = this.settingsRepository,
|
settingsRepository: SettingsRepository = this.settingsRepository,
|
||||||
|
@ -608,6 +663,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
policyManager: PolicyManager = this.policyManager,
|
policyManager: PolicyManager = this.policyManager,
|
||||||
): AccountSecurityViewModel = AccountSecurityViewModel(
|
): AccountSecurityViewModel = AccountSecurityViewModel(
|
||||||
authRepository = authRepository,
|
authRepository = authRepository,
|
||||||
|
featureFlagManager = featureFlagManager,
|
||||||
vaultRepository = vaultRepository,
|
vaultRepository = vaultRepository,
|
||||||
settingsRepository = settingsRepository,
|
settingsRepository = settingsRepository,
|
||||||
environmentRepository = environmentRepository,
|
environmentRepository = environmentRepository,
|
||||||
|
@ -648,6 +704,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||||
private val DEFAULT_STATE: AccountSecurityState = AccountSecurityState(
|
private val DEFAULT_STATE: AccountSecurityState = AccountSecurityState(
|
||||||
dialog = null,
|
dialog = null,
|
||||||
fingerprintPhrase = FINGERPRINT.asText(),
|
fingerprintPhrase = FINGERPRINT.asText(),
|
||||||
|
isAuthenticatorSyncChecked = false,
|
||||||
isUnlockWithBiometricsEnabled = false,
|
isUnlockWithBiometricsEnabled = false,
|
||||||
isUnlockWithPasswordEnabled = true,
|
isUnlockWithPasswordEnabled = true,
|
||||||
isUnlockWithPinEnabled = false,
|
isUnlockWithPinEnabled = false,
|
||||||
|
@ -656,4 +713,5 @@ private val DEFAULT_STATE: AccountSecurityState = AccountSecurityState(
|
||||||
vaultTimeoutAction = VaultTimeoutAction.LOCK,
|
vaultTimeoutAction = VaultTimeoutAction.LOCK,
|
||||||
vaultTimeoutPolicyMinutes = null,
|
vaultTimeoutPolicyMinutes = null,
|
||||||
vaultTimeoutPolicyAction = null,
|
vaultTimeoutPolicyAction = null,
|
||||||
|
shouldShowEnableAuthenticatorSync = false,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue