From 4cd5a1ed56ead66318341fa7502c3693b6541555 Mon Sep 17 00:00:00 2001 From: David Perez Date: Wed, 24 Sep 2025 14:50:13 -0500 Subject: [PATCH] PM-26025: Add browser autofill screen for onboarding flow (#5931) --- .../datasource/disk/model/OnboardingStatus.kt | 6 + .../accountsetup/SetupAutoFillViewModel.kt | 22 +- .../SetupBrowserAutofillNavigation.kt | 40 ++++ .../SetupBrowserAutofillScreen.kt | 201 ++++++++++++++++++ .../SetupBrowserAutofillViewModel.kt | 189 ++++++++++++++++ .../accountsetup/SetupUnlockViewModel.kt | 14 +- .../platform/feature/rootnav/RootNavScreen.kt | 9 + .../feature/rootnav/RootNavViewModel.kt | 29 ++- .../settings/autofill/AutoFillScreen.kt | 5 +- .../settings/autofill/AutoFillViewModel.kt | 19 +- .../browser/BrowserAutofillSettingsCard.kt | 52 ++--- ...owserThirdPartyAutofillStatusExtensions.kt | 20 ++ .../SetupAutoFillViewModelTest.kt | 57 +++-- .../SetupBrowserAutofillScreenTest.kt | 179 ++++++++++++++++ .../SetupBrowserAutofillViewModelTest.kt | 184 ++++++++++++++++ .../accountsetup/SetupUnlockViewModelTest.kt | 45 ++-- .../feature/rootnav/RootNavScreenTest.kt | 12 ++ .../feature/rootnav/RootNavViewModelTest.kt | 38 ++++ .../settings/autofill/AutoFillScreenTest.kt | 2 +- ...rThirdPartyAutofillStatusExtensionsTest.kt | 63 ++++++ ui/src/main/res/values/strings.xml | 4 +- 21 files changed, 1095 insertions(+), 95 deletions(-) create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillNavigation.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillScreen.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillViewModel.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/util/BrowserThirdPartyAutofillStatusExtensions.kt create mode 100644 app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillScreenTest.kt create mode 100644 app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillViewModelTest.kt create mode 100644 app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/util/BrowserThirdPartyAutofillStatusExtensionsTest.kt diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/model/OnboardingStatus.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/model/OnboardingStatus.kt index a870a6a21f..aeced5ec19 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/model/OnboardingStatus.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/model/OnboardingStatus.kt @@ -27,6 +27,12 @@ enum class OnboardingStatus { @SerialName("autofillSetup") AUTOFILL_SETUP, + /** + * The user is completing the browser autofill service setup. + */ + @SerialName("browserAutofillSetup") + BROWSER_AUTOFILL_SETUP, + /** * The user is completing the final step of the onboarding process. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModel.kt index 4c47465da2..bc317470c5 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import com.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import dagger.hilt.android.lifecycle.HiltViewModel @@ -27,6 +28,7 @@ class SetupAutoFillViewModel @Inject constructor( private val settingsRepository: SettingsRepository, private val authRepository: AuthRepository, private val firstTimeActionManager: FirstTimeActionManager, + private val browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager, ) : BaseViewModel( // We load the state from the savedStateHandle for testing purposes. @@ -100,13 +102,13 @@ class SetupAutoFillViewModel @Inject constructor( private fun handleTurnOnLaterConfirmClick() { firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = true) - updateOnboardingStatusToFinalStep() + updateOnboardingStatusToNextStep() } private fun handleContinueClick() { firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = false) if (state.isInitialSetup) { - updateOnboardingStatusToFinalStep() + updateOnboardingStatusToNextStep() } else { sendEvent(SetupAutoFillEvent.NavigateBack) } @@ -120,10 +122,18 @@ class SetupAutoFillViewModel @Inject constructor( } } - private fun updateOnboardingStatusToFinalStep() = - authRepository.setOnboardingStatus( - status = OnboardingStatus.FINAL_STEP, - ) + private fun updateOnboardingStatusToNextStep() { + val isAutofillEnabled = settingsRepository.isAutofillEnabledStateFlow.value + val isBrowserAutofillUnconfigured = browserThirdPartyAutofillEnabledManager + .browserThirdPartyAutofillStatus + .isAnyIsAvailableAndDisabled + val nextStep = when { + !isAutofillEnabled -> OnboardingStatus.FINAL_STEP + isBrowserAutofillUnconfigured -> OnboardingStatus.BROWSER_AUTOFILL_SETUP + else -> OnboardingStatus.FINAL_STEP + } + authRepository.setOnboardingStatus(status = nextStep) + } } /** diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillNavigation.kt new file mode 100644 index 0000000000..2ec9a33051 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillNavigation.kt @@ -0,0 +1,40 @@ +package com.x8bit.bitwarden.ui.auth.feature.accountsetup + +import android.os.Parcelable +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import com.bitwarden.ui.platform.base.util.composableWithPushTransitions +import com.bitwarden.ui.platform.util.ParcelableRouteSerializer +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +/** + * The type-safe route for the setup browser autofill screen. + */ +@Parcelize +@Serializable(with = SetupBrowserAutofillRoute.Serializer::class) +data object SetupBrowserAutofillRoute : Parcelable { + /** + * Custom serializer for this route. + */ + class Serializer : ParcelableRouteSerializer( + kClass = SetupBrowserAutofillRoute::class, + ) +} + +/** + * Navigate to the setup browser autofill screen. + */ +fun NavController.navigateToSetupBrowserAutofillScreen(navOptions: NavOptions? = null) { + this.navigate(route = SetupBrowserAutofillRoute, navOptions = navOptions) +} + +/** + * Add the setup browser autofill screen to the nav graph. + */ +fun NavGraphBuilder.setupBrowserAutofillDestination() { + composableWithPushTransitions { + SetupBrowserAutofillScreen() + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillScreen.kt new file mode 100644 index 0000000000..fdfb284583 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillScreen.kt @@ -0,0 +1,201 @@ +package com.x8bit.bitwarden.ui.auth.feature.accountsetup + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.bitwarden.ui.platform.base.util.EventsEffect +import com.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar +import com.bitwarden.ui.platform.components.button.BitwardenFilledButton +import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton +import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog +import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold +import com.bitwarden.ui.platform.composition.LocalIntentManager +import com.bitwarden.ui.platform.manager.IntentManager +import com.bitwarden.ui.platform.resource.BitwardenString +import com.bitwarden.ui.platform.theme.BitwardenTheme +import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.BrowserAutofillSettingsCard +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.model.BrowserAutofillSettingsOption +import com.x8bit.bitwarden.ui.platform.manager.utils.startBrowserAutofillSettingsActivity +import kotlinx.collections.immutable.persistentListOf + +/** + * Top level composable for the Setup Browser Autofill screen. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SetupBrowserAutofillScreen( + viewModel: SetupBrowserAutofillViewModel = hiltViewModel(), + intentManager: IntentManager = LocalIntentManager.current, +) { + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + EventsEffect(viewModel = viewModel) { event -> + when (event) { + is SetupBrowserAutofillEvent.NavigateToBrowserAutofillSettings -> { + intentManager.startBrowserAutofillSettingsActivity( + browserPackage = event.browserPackage, + ) + } + } + } + SetupBrowserAutofillDialogs( + dialogState = state.dialogState, + onDismissDialog = remember(viewModel) { + { viewModel.trySendAction(SetupBrowserAutofillAction.DismissDialog) } + }, + onTurnOnLaterConfirm = remember(viewModel) { + { viewModel.trySendAction(SetupBrowserAutofillAction.TurnOnLaterConfirmClick) } + }, + ) + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + BitwardenScaffold( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + BitwardenTopAppBar( + title = stringResource(id = BitwardenString.account_setup), + scrollBehavior = scrollBehavior, + navigationIcon = null, + ) + }, + ) { + SetupBrowserAutofillContent( + state = state, + onBrowserClick = remember(viewModel) { + { viewModel.trySendAction(SetupBrowserAutofillAction.BrowserIntegrationClick(it)) } + }, + onContinueClick = remember(viewModel) { + { viewModel.trySendAction(SetupBrowserAutofillAction.ContinueClick) } + }, + onTurnOnLaterClick = remember(viewModel) { + { viewModel.trySendAction(SetupBrowserAutofillAction.TurnOnLaterClick) } + }, + modifier = Modifier.fillMaxSize(), + ) + } +} + +@Composable +private fun SetupBrowserAutofillContent( + state: SetupBrowserAutofillState, + onBrowserClick: (BrowserPackage) -> Unit, + onContinueClick: () -> Unit, + onTurnOnLaterClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.verticalScroll(rememberScrollState()), + ) { + Spacer(Modifier.height(height = 24.dp)) + Text( + text = stringResource(id = BitwardenString.turn_on_browser_autofill_integration), + style = BitwardenTheme.typography.titleMedium, + color = BitwardenTheme.colorScheme.text.primary, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + Spacer(Modifier.height(height = 8.dp)) + Text( + text = stringResource( + id = BitwardenString.youre_using_a_browser_that_requires_special_permissions, + ), + style = BitwardenTheme.typography.bodyMedium, + color = BitwardenTheme.colorScheme.text.primary, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(height = 24.dp)) + BrowserAutofillSettingsCard( + options = state.browserAutofillSettingsOptions, + onOptionClicked = onBrowserClick, + ) + Spacer(modifier = Modifier.height(height = 24.dp)) + BitwardenFilledButton( + label = stringResource(id = BitwardenString.continue_text), + onClick = onContinueClick, + isEnabled = state.isContinueEnabled, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(height = 12.dp)) + BitwardenOutlinedButton( + label = stringResource(BitwardenString.turn_on_later), + onClick = onTurnOnLaterClick, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.navigationBarsPadding()) + } +} + +@Composable +private fun SetupBrowserAutofillDialogs( + dialogState: SetupBrowserAutofillState.DialogState?, + onTurnOnLaterConfirm: () -> Unit, + onDismissDialog: () -> Unit, +) { + when (dialogState) { + SetupBrowserAutofillState.DialogState.TurnOnLaterDialog -> { + BitwardenTwoButtonDialog( + title = stringResource(BitwardenString.turn_on_autofill_later), + message = stringResource( + id = BitwardenString.return_to_complete_this_step_anytime_in_settings, + ), + confirmButtonText = stringResource(id = BitwardenString.confirm), + dismissButtonText = stringResource(id = BitwardenString.cancel), + onConfirmClick = onTurnOnLaterConfirm, + onDismissClick = onDismissDialog, + onDismissRequest = onDismissDialog, + ) + } + + null -> Unit + } +} + +@Preview(uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun SetupBrowserAutofillContent_preview() { + BitwardenTheme { + SetupBrowserAutofillContent( + state = SetupBrowserAutofillState( + dialogState = null, + browserAutofillSettingsOptions = persistentListOf( + BrowserAutofillSettingsOption.BraveStable(enabled = true), + BrowserAutofillSettingsOption.ChromeStable(enabled = false), + BrowserAutofillSettingsOption.ChromeBeta(enabled = true), + ), + ), + onBrowserClick = { }, + onContinueClick = { }, + onTurnOnLaterClick = { }, + ) + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillViewModel.kt new file mode 100644 index 0000000000..7771afcebd --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillViewModel.kt @@ -0,0 +1,189 @@ +package com.x8bit.bitwarden.ui.auth.feature.accountsetup + +import android.os.Parcelable +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.bitwarden.ui.platform.base.BaseViewModel +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus +import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager +import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage +import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.model.BrowserAutofillSettingsOption +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.util.toBrowserAutoFillSettingsOptions +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +private const val KEY_STATE = "state" + +/** + * View model for the Setup Browser Autofill screen. + */ +@HiltViewModel +class SetupBrowserAutofillViewModel @Inject constructor( + private val authRepository: AuthRepository, + browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + // We load the state from the savedStateHandle for testing purposes. + initialState = savedStateHandle[KEY_STATE] ?: SetupBrowserAutofillState( + dialogState = null, + browserAutofillSettingsOptions = browserThirdPartyAutofillEnabledManager + .browserThirdPartyAutofillStatus + .toBrowserAutoFillSettingsOptions(), + ), +) { + init { + browserThirdPartyAutofillEnabledManager + .browserThirdPartyAutofillStatusFlow + .map(SetupBrowserAutofillAction.Internal::BrowserAutofillStatusReceive) + .onEach(::sendAction) + .launchIn(viewModelScope) + } + + override fun handleAction(action: SetupBrowserAutofillAction) { + when (action) { + is SetupBrowserAutofillAction.BrowserIntegrationClick -> { + handleBrowserIntegrationClick(action) + } + + SetupBrowserAutofillAction.DismissDialog -> handleDismissDialog() + SetupBrowserAutofillAction.ContinueClick -> handleContinueClick() + SetupBrowserAutofillAction.TurnOnLaterClick -> handleTurnOnLaterClick() + SetupBrowserAutofillAction.TurnOnLaterConfirmClick -> handleTurnOnLaterConfirmClick() + is SetupBrowserAutofillAction.Internal -> handleInternalAction(action) + } + } + + private fun handleInternalAction(action: SetupBrowserAutofillAction.Internal) { + when (action) { + is SetupBrowserAutofillAction.Internal.BrowserAutofillStatusReceive -> { + handleBrowserAutofillStatusReceive(action) + } + } + } + + private fun handleBrowserIntegrationClick( + action: SetupBrowserAutofillAction.BrowserIntegrationClick, + ) { + sendEvent( + SetupBrowserAutofillEvent.NavigateToBrowserAutofillSettings(action.browserPackage), + ) + } + + private fun handleDismissDialog() { + mutableStateFlow.update { it.copy(dialogState = null) } + } + + private fun handleContinueClick() { + authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP) + } + + private fun handleTurnOnLaterClick() { + mutableStateFlow.update { + it.copy(dialogState = SetupBrowserAutofillState.DialogState.TurnOnLaterDialog) + } + } + + private fun handleTurnOnLaterConfirmClick() { + mutableStateFlow.update { it.copy(dialogState = null) } + authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP) + } + + private fun handleBrowserAutofillStatusReceive( + action: SetupBrowserAutofillAction.Internal.BrowserAutofillStatusReceive, + ) { + mutableStateFlow.update { + it.copy( + browserAutofillSettingsOptions = action.status.toBrowserAutoFillSettingsOptions(), + ) + } + } +} + +/** + * UI State for the Setup Browser Autofill screen. + */ +@Parcelize +data class SetupBrowserAutofillState( + val dialogState: DialogState?, + val browserAutofillSettingsOptions: ImmutableList, +) : Parcelable { + /** + * Indicates if the Continue button should be enabled or not. + */ + val isContinueEnabled: Boolean get() = browserAutofillSettingsOptions.any { it.isEnabled } + + /** + * Models dialogs that can be shown on the Setup Browser Autofill screen. + */ + @Parcelize + sealed class DialogState : Parcelable { + /** + * Represents the turn on later dialog. + */ + data object TurnOnLaterDialog : DialogState() + } +} + +/** + * UI Events for the Setup Browser Autofill screen. + */ +sealed class SetupBrowserAutofillEvent { + /** + * Navigate to the Autofill settings of the specified [browserPackage]. + */ + data class NavigateToBrowserAutofillSettings( + val browserPackage: BrowserPackage, + ) : SetupBrowserAutofillEvent() +} + +/** + * UI Actions for the Setup Browser Autofill screen. + */ +sealed class SetupBrowserAutofillAction { + /** + * Indicates that a browser integration toggle was clicked. + */ + data class BrowserIntegrationClick( + val browserPackage: BrowserPackage, + ) : SetupBrowserAutofillAction() + + /** + * Indicates that the dialog has been dismissed. + */ + data object DismissDialog : SetupBrowserAutofillAction() + + /** + * Indicates that the "Continue" button was clicked. + */ + data object ContinueClick : SetupBrowserAutofillAction() + + /** + * Indicates that the "Turn on later" button was clicked. + */ + data object TurnOnLaterClick : SetupBrowserAutofillAction() + + /** + * Indicates that the confirmation button was clicked to turn on later. + */ + data object TurnOnLaterConfirmClick : SetupBrowserAutofillAction() + + /** + * Models actions the [SetupBrowserAutofillViewModel] itself may send. + */ + sealed class Internal : SetupBrowserAutofillAction() { + /** + * Received updated [BrowserThirdPartyAutofillStatus] data. + */ + data class BrowserAutofillStatusReceive( + val status: BrowserThirdPartyAutofillStatus, + ) : Internal() + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt index 0911927b84..7e39124488 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt @@ -9,6 +9,7 @@ import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.repository.SettingsRepository @@ -34,6 +35,7 @@ class SetupUnlockViewModel @Inject constructor( private val settingsRepository: SettingsRepository, private val biometricsEncryptionManager: BiometricsEncryptionManager, private val firstTimeActionManager: FirstTimeActionManager, + private val browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager, ) : BaseViewModel( // We load the state from the savedStateHandle for testing purposes. initialState = savedStateHandle[KEY_STATE] ?: run { @@ -203,10 +205,14 @@ class SetupUnlockViewModel @Inject constructor( } private fun updateOnboardingStatusToNextStep() { - val nextStep = if (settingsRepository.isAutofillEnabledStateFlow.value) { - OnboardingStatus.FINAL_STEP - } else { - OnboardingStatus.AUTOFILL_SETUP + val isAutofillEnabled = settingsRepository.isAutofillEnabledStateFlow.value + val isBrowserAutofillUnconfigured = browserThirdPartyAutofillEnabledManager + .browserThirdPartyAutofillStatus + .isAnyIsAvailableAndDisabled + val nextStep = when { + !isAutofillEnabled -> OnboardingStatus.AUTOFILL_SETUP + isBrowserAutofillUnconfigured -> OnboardingStatus.BROWSER_AUTOFILL_SETUP + else -> OnboardingStatus.FINAL_STEP } authRepository.setOnboardingStatus(nextStep) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt index 54be454f6e..e779b45308 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt @@ -18,12 +18,15 @@ import com.bitwarden.ui.platform.theme.NonNullExitTransitionProvider import com.bitwarden.ui.platform.theme.RootTransitionProviders import com.bitwarden.ui.platform.util.toObjectNavigationRoute import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupAutofillRoute +import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupBrowserAutofillRoute import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupCompleteRoute import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupUnlockRoute import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupAutoFillAsRootScreen +import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupBrowserAutofillScreen import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupCompleteScreen import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupUnlockScreenAsRoot import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupAutoFillDestinationAsRoot +import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupBrowserAutofillDestination import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupCompleteDestination import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupUnlockDestinationAsRoot import com.x8bit.bitwarden.ui.auth.feature.auth.AuthGraphRoute @@ -107,6 +110,7 @@ fun RootNavScreen( vaultUnlockDestination() vaultUnlockedGraph(navController) setupUnlockDestinationAsRoot() + setupBrowserAutofillDestination() setupAutoFillDestinationAsRoot() setupCompleteDestination() exportItemsGraph() @@ -140,6 +144,7 @@ fun RootNavScreen( RootNavState.OnboardingAccountLockSetup -> SetupUnlockRoute.AsRoot RootNavState.OnboardingAutoFillSetup -> SetupAutofillRoute.AsRoot + RootNavState.OnboardingBrowserAutofillSetup -> SetupBrowserAutofillRoute RootNavState.OnboardingStepsComplete -> SetupCompleteRoute } val currentRoute = navController.currentDestination?.rootLevelRoute() @@ -271,6 +276,10 @@ fun RootNavScreen( navController.navigateToSetupAutoFillAsRootScreen(rootNavOptions) } + RootNavState.OnboardingBrowserAutofillSetup -> { + navController.navigateToSetupBrowserAutofillScreen(rootNavOptions) + } + RootNavState.OnboardingStepsComplete -> { navController.navigateToSetupCompleteScreen(rootNavOptions) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt index b741255088..1310edef33 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt @@ -99,15 +99,7 @@ class RootNavViewModel @Inject constructor( userState.activeAccount.isVaultUnlocked && userState.activeAccount.onboardingStatus != OnboardingStatus.COMPLETE -> { - when (userState.activeAccount.onboardingStatus) { - OnboardingStatus.NOT_STARTED, - OnboardingStatus.ACCOUNT_LOCK_SETUP, - -> RootNavState.OnboardingAccountLockSetup - - OnboardingStatus.AUTOFILL_SETUP -> RootNavState.OnboardingAutoFillSetup - OnboardingStatus.FINAL_STEP -> RootNavState.OnboardingStepsComplete - OnboardingStatus.COMPLETE -> throw IllegalStateException("Should not have entered here.") - } + getOnboardingNavState(onboardingStatus = userState.activeAccount.onboardingStatus) } userState.activeAccount.isVaultUnlocked -> { @@ -200,6 +192,19 @@ class RootNavViewModel @Inject constructor( mutableStateFlow.update { updatedRootNavState } } + private fun getOnboardingNavState( + onboardingStatus: OnboardingStatus, + ): RootNavState = when (onboardingStatus) { + OnboardingStatus.NOT_STARTED, + OnboardingStatus.ACCOUNT_LOCK_SETUP, + -> RootNavState.OnboardingAccountLockSetup + + OnboardingStatus.AUTOFILL_SETUP -> RootNavState.OnboardingAutoFillSetup + OnboardingStatus.BROWSER_AUTOFILL_SETUP -> RootNavState.OnboardingBrowserAutofillSetup + OnboardingStatus.FINAL_STEP -> RootNavState.OnboardingStepsComplete + OnboardingStatus.COMPLETE -> throw IllegalStateException("Should not have entered here.") + } + private fun getRegistrationEventNavState( registrationEvent: SpecialCircumstance.RegistrationEvent, ): RootNavState = when (registrationEvent) { @@ -402,6 +407,12 @@ sealed class RootNavState : Parcelable { @Parcelize data object OnboardingAutoFillSetup : RootNavState() + /** + * App should show the set up browser autofill onboarding screen. + */ + @Parcelize + data object OnboardingBrowserAutofillSetup : RootNavState() + /** * App should show the onboarding steps complete screen. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt index 10bdc9a586..b451d7a1b8 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt @@ -227,7 +227,10 @@ private fun AutoFillScreenContent( BrowserAutofillSettingsCard( options = state.browserAutofillSettingsOptions, onOptionClicked = autoFillHandlers.onBrowserAutofillSelected, - enabled = state.isAutoFillServicesEnabled, + supportingText = stringResource( + id = BitwardenString + .improves_login_filling_for_supported_websites_on_selected_browsers, + ), ) Spacer(modifier = Modifier.height(8.dp)) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt index 53733cb2c7..6ca7b1174c 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt @@ -5,7 +5,6 @@ import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.bitwarden.core.util.isBuildVersionAtLeast -import com.bitwarden.core.util.persistentListOfNotNull import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text @@ -18,6 +17,7 @@ import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.model.BrowserAutofillSettingsOption +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.util.toBrowserAutoFillSettingsOptions import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.launchIn @@ -303,23 +303,6 @@ enum class AutofillStyle(val label: Text) { POPUP(label = BitwardenString.autofill_suggestions_popup.asText()), } -@Suppress("MaxLineLength") -private fun BrowserThirdPartyAutofillStatus.toBrowserAutoFillSettingsOptions(): ImmutableList = - persistentListOfNotNull( - BrowserAutofillSettingsOption.BraveStable( - enabled = this.braveStableStatusData.isThirdPartyEnabled, - ) - .takeIf { this.braveStableStatusData.isAvailable }, - BrowserAutofillSettingsOption.ChromeStable( - enabled = this.chromeStableStatusData.isThirdPartyEnabled, - ) - .takeIf { this.chromeStableStatusData.isAvailable }, - BrowserAutofillSettingsOption.ChromeBeta( - enabled = this.chromeBetaChannelStatusData.isThirdPartyEnabled, - ) - .takeIf { this.chromeBetaChannelStatusData.isAvailable }, - ) - /** * Models events for the auto-fill screen. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/BrowserAutofillSettingsCard.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/BrowserAutofillSettingsCard.kt index 2bdf17ba88..415e238d15 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/BrowserAutofillSettingsCard.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/BrowserAutofillSettingsCard.kt @@ -6,14 +6,13 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.bitwarden.ui.platform.base.util.cardStyle import com.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.bitwarden.ui.platform.base.util.toListItemCardStyle import com.bitwarden.ui.platform.components.model.CardStyle import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch -import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.model.BrowserAutofillSettingsOption @@ -27,14 +26,14 @@ import kotlinx.collections.immutable.persistentListOf * @param options List of data to display in the card, if the list is empty nothing will be drawn. * @param onOptionClicked Lambda that is invoked when an option row is clicked and passes back the * [BrowserPackage] for that option. - * @param enabled Whether to show the switches for each option as enabled. + * @param supportingText The optional supporting text in the card. */ @Composable fun BrowserAutofillSettingsCard( options: ImmutableList, onOptionClicked: (BrowserPackage) -> Unit, - enabled: Boolean, modifier: Modifier = Modifier, + supportingText: String? = null, ) { if (options.isEmpty()) return Column(modifier = modifier) { @@ -45,37 +44,31 @@ fun BrowserAutofillSettingsCard( onCheckedChange = { onOptionClicked(option.browserPackage) }, - cardStyle = if (index == 0) { - CardStyle.Top( - dividerPadding = 16.dp, - ) - } else { - CardStyle.Middle( - dividerPadding = 16.dp, - ) + cardStyle = when { + supportingText == null -> options.toListItemCardStyle(index = index) + index == 0 -> CardStyle.Top(dividerPadding = 16.dp) + else -> CardStyle.Middle(dividerPadding = 16.dp) }, - enabled = enabled, modifier = Modifier .fillMaxWidth() .standardHorizontalMargin(), ) } - Text( - text = stringResource( - id = BitwardenString - .improves_login_filling_for_supported_websites_on_selected_browsers, - ), - style = BitwardenTheme.typography.bodyMedium, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .cardStyle( - cardStyle = CardStyle.Bottom, - paddingHorizontal = 16.dp, - ) - .defaultMinSize(minHeight = 48.dp), - ) + supportingText?.let { + Text( + text = it, + style = BitwardenTheme.typography.bodyMedium, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .cardStyle( + cardStyle = CardStyle.Bottom, + paddingHorizontal = 16.dp, + ) + .defaultMinSize(minHeight = 48.dp), + ) + } } } @@ -89,7 +82,6 @@ private fun ChromeAutofillSettingsCard_preview() { BrowserAutofillSettingsOption.ChromeStable(enabled = false), BrowserAutofillSettingsOption.ChromeBeta(enabled = true), ), - enabled = true, onOptionClicked = {}, ) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/util/BrowserThirdPartyAutofillStatusExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/util/BrowserThirdPartyAutofillStatusExtensions.kt new file mode 100644 index 0000000000..fc98f1dfdf --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/util/BrowserThirdPartyAutofillStatusExtensions.kt @@ -0,0 +1,20 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.util + +import com.bitwarden.core.util.persistentListOfNotNull +import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.model.BrowserAutofillSettingsOption +import kotlinx.collections.immutable.ImmutableList + +/** + * Converts a [BrowserThirdPartyAutofillStatus] to a list of [BrowserAutofillSettingsOption]. + */ +@Suppress("MaxLineLength") +fun BrowserThirdPartyAutofillStatus.toBrowserAutoFillSettingsOptions(): ImmutableList = + persistentListOfNotNull( + BrowserAutofillSettingsOption.BraveStable(braveStableStatusData.isThirdPartyEnabled) + .takeIf { this.braveStableStatusData.isAvailable }, + BrowserAutofillSettingsOption.ChromeStable(chromeStableStatusData.isThirdPartyEnabled) + .takeIf { this.chromeStableStatusData.isAvailable }, + BrowserAutofillSettingsOption.ChromeBeta(chromeBetaChannelStatusData.isThirdPartyEnabled) + .takeIf { this.chromeBetaChannelStatusData.isAvailable }, + ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModelTest.kt index ad2cb80a8f..3fc93739f7 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModelTest.kt @@ -6,6 +6,7 @@ import com.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState +import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState import com.x8bit.bitwarden.data.platform.repository.SettingsRepository @@ -48,6 +49,11 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() { every { userStateFlow } returns mutableUserStateFlow every { setOnboardingStatus(any()) } just runs } + private val thirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager = mockk { + every { + browserThirdPartyAutofillStatus + } returns mockk { every { isAnyIsAvailableAndDisabled } returns false } + } @BeforeEach fun setup() { @@ -131,10 +137,24 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() { fun `handleTurnOnLaterConfirmClick sets onboarding status to FINAL_STEP`() { val viewModel = createViewModel() viewModel.trySendAction(SetupAutoFillAction.TurnOnLaterConfirmClick) - verify { - authRepository.setOnboardingStatus( - OnboardingStatus.FINAL_STEP, - ) + verify(exactly = 1) { + authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP) + thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus + firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = true) + } + } + + @Test + fun `handleTurnOnLaterConfirmClick sets onboarding status to BROWSER_AUTOFILL_SETUP`() { + every { + thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus + } returns mockk { every { isAnyIsAvailableAndDisabled } returns true } + val viewModel = createViewModel() + viewModel.trySendAction(SetupAutoFillAction.TurnOnLaterConfirmClick) + verify(exactly = 1) { + authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP) + thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus + firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = true) } } @@ -144,9 +164,22 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() { val viewModel = createViewModel() viewModel.trySendAction(SetupAutoFillAction.ContinueClick) verify(exactly = 1) { - authRepository.setOnboardingStatus( - OnboardingStatus.FINAL_STEP, - ) + authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP) + thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus + firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = false) + } + } + + @Test + fun `handleContinueClick sets onboarding status to BROWSER_AUTOFILL_SETUP`() { + every { + thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus + } returns mockk { every { isAnyIsAvailableAndDisabled } returns true } + val viewModel = createViewModel() + viewModel.trySendAction(SetupAutoFillAction.ContinueClick) + verify(exactly = 1) { + authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP) + thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = false) } } @@ -155,8 +188,9 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() { @Test fun `handleContinueClick send NavigateBack event when not initial setup and sets first time flag to false`() = runTest { - val viewModel = - createViewModel(initialState = DEFAULT_STATE.copy(isInitialSetup = false)) + val viewModel = createViewModel( + initialState = DEFAULT_STATE.copy(isInitialSetup = false), + ) viewModel.eventFlow.test { viewModel.trySendAction(SetupAutoFillAction.ContinueClick) assertEquals( @@ -168,9 +202,7 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() { firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = false) } verify(exactly = 0) { - authRepository.setOnboardingStatus( - OnboardingStatus.FINAL_STEP, - ) + authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP) } } @@ -204,6 +236,7 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() { settingsRepository = settingsRepository, authRepository = authRepository, firstTimeActionManager = firstTimeActionManager, + browserThirdPartyAutofillEnabledManager = thirdPartyAutofillEnabledManager, ) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillScreenTest.kt new file mode 100644 index 0000000000..8ee11070c4 --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillScreenTest.kt @@ -0,0 +1,179 @@ +package com.x8bit.bitwarden.ui.auth.feature.accountsetup + +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.isDialog +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow +import com.bitwarden.ui.platform.manager.IntentManager +import com.bitwarden.ui.util.assertNoDialogExists +import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage +import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.model.BrowserAutofillSettingsOption +import com.x8bit.bitwarden.ui.platform.manager.utils.startBrowserAutofillSettingsActivity +import com.x8bit.bitwarden.ui.platform.manager.utils.startSystemAutofillSettingsActivity +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.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import org.junit.After +import org.junit.Before +import org.junit.Test + +class SetupBrowserAutofillScreenTest : BitwardenComposeTest() { + private val intentManager = mockk() + + private val mutableEventFlow = bufferedMutableSharedFlow() + private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) + private val viewModel = mockk { + every { eventFlow } returns mutableEventFlow + every { stateFlow } returns mutableStateFlow + every { trySendAction(action = any()) } just runs + } + + @Before + fun setup() { + mockkStatic(IntentManager::startSystemAutofillSettingsActivity) + setContent( + intentManager = intentManager, + ) { + SetupBrowserAutofillScreen( + viewModel = viewModel, + ) + } + } + + @After + fun tearDown() { + unmockkStatic(IntentManager::startSystemAutofillSettingsActivity) + } + + @Test + fun `NavigateToBrowserAutofillSettings should start system autofill settings activity`() { + val browserPackage = BrowserPackage.CHROME_STABLE + every { intentManager.startBrowserAutofillSettingsActivity(browserPackage) } returns true + mutableEventFlow.tryEmit( + value = SetupBrowserAutofillEvent.NavigateToBrowserAutofillSettings(browserPackage), + ) + verify(exactly = 1) { + intentManager.startBrowserAutofillSettingsActivity(browserPackage) + } + } + + @Test + fun `BrowserIntegrationClick should emit when integration row is clicked`() { + composeTestRule + .onNodeWithText(text = "Use Brave autofill integration") + .performScrollTo() + .performClick() + + verify(exactly = 1) { + viewModel.trySendAction( + SetupBrowserAutofillAction.BrowserIntegrationClick(BrowserPackage.BRAVE_RELEASE), + ) + } + } + + @Test + fun `continue button is enabled or disabled according to state`() { + composeTestRule + .onNodeWithText(text = "Continue") + .assertIsEnabled() + mutableStateFlow.update { + it.copy( + browserAutofillSettingsOptions = persistentListOf( + BrowserAutofillSettingsOption.BraveStable(enabled = false), + ), + ) + } + composeTestRule + .onNodeWithText(text = "Continue") + .assertIsNotEnabled() + } + + @Test + fun `ContinueClick should emit when enabled and clicked`() { + composeTestRule + .onNodeWithText(text = "Continue") + .performScrollTo() + .performClick() + + verify(exactly = 1) { + viewModel.trySendAction(SetupBrowserAutofillAction.ContinueClick) + } + } + + @Test + fun `TurnOnLaterClick should emit when clicked`() { + composeTestRule + .onNodeWithText(text = "Turn on later") + .performScrollTo() + .performClick() + + verify(exactly = 1) { + viewModel.trySendAction(SetupBrowserAutofillAction.TurnOnLaterClick) + } + } + + @Test + fun `correct dialog should be displayed according to state`() { + composeTestRule.assertNoDialogExists() + mutableStateFlow.update { + it.copy(dialogState = SetupBrowserAutofillState.DialogState.TurnOnLaterDialog) + } + + composeTestRule.onNode(isDialog()).assertExists() + + mutableStateFlow.update { it.copy(dialogState = null) } + composeTestRule.assertNoDialogExists() + } + + @Test + fun `DismissDialog should emit when dialog is dismissed`() { + mutableStateFlow.update { + it.copy(dialogState = SetupBrowserAutofillState.DialogState.TurnOnLaterDialog) + } + + composeTestRule + .onNodeWithText(text = "Cancel") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + verify(exactly = 1) { + viewModel.trySendAction(SetupBrowserAutofillAction.DismissDialog) + } + } + + @Test + fun `TurnOnLaterConfirmClick should emit when dialog is confirmed`() { + mutableStateFlow.update { + it.copy(dialogState = SetupBrowserAutofillState.DialogState.TurnOnLaterDialog) + } + + composeTestRule + .onNodeWithText(text = "Confirm") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + verify(exactly = 1) { + viewModel.trySendAction(SetupBrowserAutofillAction.TurnOnLaterConfirmClick) + } + } +} + +private val DEFAULT_STATE: SetupBrowserAutofillState = SetupBrowserAutofillState( + dialogState = null, + browserAutofillSettingsOptions = persistentListOf( + BrowserAutofillSettingsOption.BraveStable(enabled = true), + ), +) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillViewModelTest.kt new file mode 100644 index 0000000000..31a44d7cb4 --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillViewModelTest.kt @@ -0,0 +1,184 @@ +package com.x8bit.bitwarden.ui.auth.feature.accountsetup + +import androidx.lifecycle.SavedStateHandle +import app.cash.turbine.test +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus +import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager +import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage +import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutoFillData +import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.model.BrowserAutofillSettingsOption +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class SetupBrowserAutofillViewModelTest { + private val authRepository: AuthRepository = mockk { + every { setOnboardingStatus(status = any()) } just runs + } + private val mutableBrowserThirdPartyAutofillStatusFlow = + MutableStateFlow(DEFAULT_BROWSER_AUTOFILL_STATUS) + private val thirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager = mockk { + every { + browserThirdPartyAutofillStatus + } answers { mutableBrowserThirdPartyAutofillStatusFlow.value } + every { + browserThirdPartyAutofillStatusFlow + } returns mutableBrowserThirdPartyAutofillStatusFlow + } + + @Test + fun `browserThirdPartyAutofillStatusFlow should update the state`() = runTest { + val viewModel = createViewModel() + viewModel.stateFlow.test { + assertEquals(DEFAULT_STATE, awaitItem()) + mutableBrowserThirdPartyAutofillStatusFlow.value = DEFAULT_BROWSER_AUTOFILL_STATUS.copy( + braveStableStatusData = BrowserThirdPartyAutoFillData( + isAvailable = true, + isThirdPartyEnabled = true, + ), + ) + assertEquals( + DEFAULT_STATE.copy( + browserAutofillSettingsOptions = persistentListOf( + BrowserAutofillSettingsOption.BraveStable(enabled = true), + BrowserAutofillSettingsOption.ChromeStable(enabled = false), + BrowserAutofillSettingsOption.ChromeBeta(enabled = false), + ), + ), + awaitItem(), + ) + mutableBrowserThirdPartyAutofillStatusFlow.value = DEFAULT_BROWSER_AUTOFILL_STATUS.copy( + braveStableStatusData = BrowserThirdPartyAutoFillData( + isAvailable = false, + isThirdPartyEnabled = false, + ), + ) + assertEquals( + DEFAULT_STATE.copy( + browserAutofillSettingsOptions = persistentListOf( + BrowserAutofillSettingsOption.ChromeStable(enabled = false), + BrowserAutofillSettingsOption.ChromeBeta(enabled = false), + ), + ), + awaitItem(), + ) + } + } + + @Test + fun `BrowserIntegrationClick should send NavigateToBrowserAutofillSettings event`() = runTest { + val browserPackage = BrowserPackage.BRAVE_RELEASE + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction( + SetupBrowserAutofillAction.BrowserIntegrationClick(browserPackage), + ) + assertEquals( + SetupBrowserAutofillEvent.NavigateToBrowserAutofillSettings(browserPackage), + awaitItem(), + ) + } + } + + @Test + fun `DismissDialog should clear the dialog state`() = runTest { + val initialState = DEFAULT_STATE.copy( + dialogState = SetupBrowserAutofillState.DialogState.TurnOnLaterDialog, + ) + val viewModel = createViewModel(initialState = initialState) + viewModel.stateFlow.test { + assertEquals(initialState, awaitItem()) + viewModel.trySendAction(SetupBrowserAutofillAction.DismissDialog) + assertEquals( + initialState.copy(dialogState = null), + awaitItem(), + ) + } + } + + @Test + fun `handleContinueClick should set the onboarding state to FINAL_STEP`() { + val viewModel = createViewModel() + viewModel.trySendAction(SetupBrowserAutofillAction.ContinueClick) + verify(exactly = 1) { + authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP) + } + } + + @Test + fun `TurnOnLaterClick should set the onboarding state to FINAL_STEP`() = runTest { + val viewModel = createViewModel() + viewModel.stateFlow.test { + assertEquals(DEFAULT_STATE, awaitItem()) + viewModel.trySendAction(SetupBrowserAutofillAction.TurnOnLaterClick) + assertEquals( + DEFAULT_STATE.copy( + dialogState = SetupBrowserAutofillState.DialogState.TurnOnLaterDialog, + ), + awaitItem(), + ) + } + } + + @Test + fun `TurnOnLaterConfirmClick should set the onboarding state to FINAL_STEP`() = runTest { + val initialState = DEFAULT_STATE.copy( + dialogState = SetupBrowserAutofillState.DialogState.TurnOnLaterDialog, + ) + val viewModel = createViewModel(initialState = initialState) + viewModel.stateFlow.test { + assertEquals(initialState, awaitItem()) + viewModel.trySendAction(SetupBrowserAutofillAction.TurnOnLaterConfirmClick) + assertEquals( + initialState.copy(dialogState = null), + awaitItem(), + ) + } + verify(exactly = 1) { + authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP) + } + } + + private fun createViewModel( + initialState: SetupBrowserAutofillState? = null, + ): SetupBrowserAutofillViewModel = SetupBrowserAutofillViewModel( + savedStateHandle = SavedStateHandle().apply { + set(key = "state", value = initialState) + }, + authRepository = authRepository, + browserThirdPartyAutofillEnabledManager = thirdPartyAutofillEnabledManager, + ) +} + +private val DEFAULT_BROWSER_AUTOFILL_STATUS = BrowserThirdPartyAutofillStatus( + braveStableStatusData = BrowserThirdPartyAutoFillData( + isAvailable = true, + isThirdPartyEnabled = false, + ), + chromeStableStatusData = BrowserThirdPartyAutoFillData( + isAvailable = true, + isThirdPartyEnabled = false, + ), + chromeBetaChannelStatusData = BrowserThirdPartyAutoFillData( + isAvailable = true, + isThirdPartyEnabled = false, + ), +) + +private val DEFAULT_STATE = SetupBrowserAutofillState( + dialogState = null, + browserAutofillSettingsOptions = persistentListOf( + BrowserAutofillSettingsOption.BraveStable(enabled = false), + BrowserAutofillSettingsOption.ChromeStable(enabled = false), + BrowserAutofillSettingsOption.ChromeBeta(enabled = false), + ), +) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt index 5203204c32..5dd78563ad 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt @@ -9,6 +9,7 @@ import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState +import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState @@ -59,6 +60,11 @@ class SetupUnlockViewModelTest : BaseViewModelTest() { } returns false every { createCipherOrNull(DEFAULT_USER_ID) } returns CIPHER } + private val thirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager = mockk { + every { + browserThirdPartyAutofillStatus + } returns mockk { every { isAnyIsAvailableAndDisabled } returns false } + } @BeforeEach fun setup() { @@ -90,10 +96,9 @@ class SetupUnlockViewModelTest : BaseViewModelTest() { fun `ContinueClick should call setOnboardingStatus and set to AUTOFILL_SETUP if AutoFill is not enabled`() { val viewModel = createViewModel() viewModel.trySendAction(SetupUnlockAction.ContinueClick) - verify { - authRepository.setOnboardingStatus( - status = OnboardingStatus.AUTOFILL_SETUP, - ) + verify(exactly = 1) { + thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus + authRepository.setOnboardingStatus(status = OnboardingStatus.AUTOFILL_SETUP) } } @@ -131,29 +136,42 @@ class SetupUnlockViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `ContinueClick should call setOnboardingStatus and set to FINAL_STEP if AutoFill is already enabled and set first time value to false`() { + fun `ContinueClick should call setOnboardingStatus and set to FINAL_STEP if AutoFill is already enabled, browsers are setup, and set first time value to false`() { mutableAutofillEnabledStateFlow.update { true } val viewModel = createViewModel() viewModel.trySendAction(SetupUnlockAction.ContinueClick) verify(exactly = 1) { - authRepository.setOnboardingStatus( - status = OnboardingStatus.FINAL_STEP, - ) + thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus + authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP) firstTimeActionManager.storeShowUnlockSettingBadge(showBadge = false) } } @Suppress("MaxLineLength") @Test - fun `SetUpLaterClick should call setOnboardingStatus and set to FINAL_STEP if AutoFill is already enabled`() = + fun `ContinueClick should call setOnboardingStatus and set to BROWSER_AUTOFILL_SETUP if AutoFill is already enabled and browsers are not setup`() { + mutableAutofillEnabledStateFlow.update { true } + every { + thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus + } returns mockk { every { isAnyIsAvailableAndDisabled } returns true } + val viewModel = createViewModel() + viewModel.trySendAction(SetupUnlockAction.ContinueClick) + verify(exactly = 1) { + thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus + authRepository.setOnboardingStatus(status = OnboardingStatus.BROWSER_AUTOFILL_SETUP) + } + } + + @Suppress("MaxLineLength") + @Test + fun `SetUpLaterClick should call setOnboardingStatus and set to FINAL_STEP if AutoFill is already enabled and browsers are setup`() = runTest { mutableAutofillEnabledStateFlow.update { true } val viewModel = createViewModel() viewModel.trySendAction(SetupUnlockAction.SetUpLaterClick) - verify { - authRepository.setOnboardingStatus( - status = OnboardingStatus.FINAL_STEP, - ) + verify(exactly = 1) { + thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus + authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP) } } @@ -395,6 +413,7 @@ class SetupUnlockViewModelTest : BaseViewModelTest() { settingsRepository = settingsRepository, biometricsEncryptionManager = biometricsEncryptionManager, firstTimeActionManager = firstTimeActionManager, + browserThirdPartyAutofillEnabledManager = thirdPartyAutofillEnabledManager, ) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt index 2560a73509..545ec3cb05 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt @@ -5,6 +5,7 @@ import com.bitwarden.ui.platform.base.createMockNavHostController import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupAutofillRoute +import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupBrowserAutofillRoute import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupCompleteRoute import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupUnlockRoute import com.x8bit.bitwarden.ui.auth.feature.auth.AuthGraphRoute @@ -417,6 +418,17 @@ class RootNavScreenTest : BitwardenComposeTest() { } } + // Make sure navigating to browser autofill setup works as expected: + rootNavStateFlow.value = RootNavState.OnboardingBrowserAutofillSetup + composeTestRule.runOnIdle { + verify { + mockNavHostController.navigate( + route = SetupBrowserAutofillRoute, + navOptions = expectedNavOptions, + ) + } + } + // Make sure navigating to account setup complete works as expected: rootNavStateFlow.value = RootNavState.OnboardingStepsComplete composeTestRule.runOnIdle { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt index bde6d03529..330ec3beaf 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt @@ -1241,6 +1241,44 @@ class RootNavViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `when the active user has an unlocked vault and they have a OnboardingStatus of BROWSER_AUTOFILL_SETUP the nav state should be OnboardingBrowserAutofillSetup`() { + mutableUserStateFlow.tryEmit( + UserState( + activeUserId = "activeUserId", + accounts = listOf( + UserState.Account( + userId = "activeUserId", + name = "name", + email = "email", + avatarColorHex = "avatarColorHex", + environment = Environment.Us, + isPremium = true, + isLoggedIn = true, + isVaultUnlocked = true, + needsPasswordReset = false, + isBiometricsEnabled = false, + organizations = emptyList(), + needsMasterPassword = false, + trustedDevice = null, + hasMasterPassword = true, + isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.BROWSER_AUTOFILL_SETUP, + firstTimeState = FirstTimeState( + showImportLoginsCard = true, + ), + ), + ), + ), + ) + val viewModel = createViewModel() + assertEquals( + RootNavState.OnboardingBrowserAutofillSetup, + viewModel.stateFlow.value, + ) + } + @Suppress("MaxLineLength") @Test fun `when the active user has an unlocked vault and they have a OnboardingStatus of FINAL_STEP the nav state should be OnboardingAutoFillSetup`() { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt index cc903c62b0..4331bd0cad 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt @@ -611,7 +611,7 @@ class AutoFillScreenTest : BitwardenComposeTest() { .performClick() composeTestRule - .onNodeWithText("Use Chrome autofill integration (Beta)") + .onNodeWithText("Use Chrome Beta autofill integration") .performScrollTo() .performClick() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/util/BrowserThirdPartyAutofillStatusExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/util/BrowserThirdPartyAutofillStatusExtensionsTest.kt new file mode 100644 index 0000000000..11608d0c9f --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/util/BrowserThirdPartyAutofillStatusExtensionsTest.kt @@ -0,0 +1,63 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.util + +import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutoFillData +import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.model.BrowserAutofillSettingsOption +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class BrowserThirdPartyAutofillStatusExtensionsTest { + @Test + fun `toBrowserAutoFillSettingsOptions should be empty if no options are available`() { + val browserThirdPartyAutofillStatus = BrowserThirdPartyAutofillStatus( + braveStableStatusData = BrowserThirdPartyAutoFillData( + isAvailable = false, + isThirdPartyEnabled = false, + ), + chromeStableStatusData = BrowserThirdPartyAutoFillData( + isAvailable = false, + isThirdPartyEnabled = false, + ), + chromeBetaChannelStatusData = BrowserThirdPartyAutoFillData( + isAvailable = false, + isThirdPartyEnabled = false, + ), + ) + + val result = browserThirdPartyAutofillStatus.toBrowserAutoFillSettingsOptions() + + assertTrue(result.isEmpty()) + } + + @Suppress("MaxLineLength") + @Test + fun `toBrowserAutoFillSettingsOptions should contain all options if all options are available`() { + val browserThirdPartyAutofillStatus = BrowserThirdPartyAutofillStatus( + braveStableStatusData = BrowserThirdPartyAutoFillData( + isAvailable = true, + isThirdPartyEnabled = false, + ), + chromeStableStatusData = BrowserThirdPartyAutoFillData( + isAvailable = true, + isThirdPartyEnabled = true, + ), + chromeBetaChannelStatusData = BrowserThirdPartyAutoFillData( + isAvailable = true, + isThirdPartyEnabled = false, + ), + ) + + val result = browserThirdPartyAutofillStatus.toBrowserAutoFillSettingsOptions() + + assertEquals( + persistentListOf( + BrowserAutofillSettingsOption.BraveStable(enabled = false), + BrowserAutofillSettingsOption.ChromeStable(enabled = true), + BrowserAutofillSettingsOption.ChromeBeta(enabled = false), + ), + result, + ) + } +} diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index d182e858a7..84008041c1 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -758,6 +758,8 @@ Do you want to switch to this account? Allow authenticator syncing There was an issue validating the registration token. Turn on autofill + Turn on browser autofill integration + You’re using a browser that requires special permissions for Bitwarden to autofill your passwords. Enable your preferred autofill integration below. Use autofill to log into your accounts with a single tap. Turn on later Turn on autofill later? @@ -906,7 +908,7 @@ Do you want to switch to this account? Self-host server URL Use Brave autofill integration Use Chrome autofill integration - Use Chrome autofill integration (Beta) + Use Chrome Beta autofill integration Improves login filling for supported websites on selected browsers. Once enabled, you’ll be directed to browser settings to enable third-party autofill. Show more No folder