PM-26025: Add browser autofill screen for onboarding flow (#5931)

This commit is contained in:
David Perez 2025-09-24 14:50:13 -05:00 committed by GitHub
parent c122f83fa6
commit 4cd5a1ed56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1095 additions and 95 deletions

View File

@ -27,6 +27,12 @@ enum class OnboardingStatus {
@SerialName("autofillSetup") @SerialName("autofillSetup")
AUTOFILL_SETUP, 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. * The user is completing the final step of the onboarding process.
*/ */

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus 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.AuthRepository
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -27,6 +28,7 @@ class SetupAutoFillViewModel @Inject constructor(
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val authRepository: AuthRepository, private val authRepository: AuthRepository,
private val firstTimeActionManager: FirstTimeActionManager, private val firstTimeActionManager: FirstTimeActionManager,
private val browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
) : ) :
BaseViewModel<SetupAutoFillState, SetupAutoFillEvent, SetupAutoFillAction>( BaseViewModel<SetupAutoFillState, SetupAutoFillEvent, SetupAutoFillAction>(
// We load the state from the savedStateHandle for testing purposes. // We load the state from the savedStateHandle for testing purposes.
@ -100,13 +102,13 @@ class SetupAutoFillViewModel @Inject constructor(
private fun handleTurnOnLaterConfirmClick() { private fun handleTurnOnLaterConfirmClick() {
firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = true) firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = true)
updateOnboardingStatusToFinalStep() updateOnboardingStatusToNextStep()
} }
private fun handleContinueClick() { private fun handleContinueClick() {
firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = false) firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = false)
if (state.isInitialSetup) { if (state.isInitialSetup) {
updateOnboardingStatusToFinalStep() updateOnboardingStatusToNextStep()
} else { } else {
sendEvent(SetupAutoFillEvent.NavigateBack) sendEvent(SetupAutoFillEvent.NavigateBack)
} }
@ -120,10 +122,18 @@ class SetupAutoFillViewModel @Inject constructor(
} }
} }
private fun updateOnboardingStatusToFinalStep() = private fun updateOnboardingStatusToNextStep() {
authRepository.setOnboardingStatus( val isAutofillEnabled = settingsRepository.isAutofillEnabledStateFlow.value
status = OnboardingStatus.FINAL_STEP, 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)
}
} }
/** /**

View File

@ -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<SetupBrowserAutofillRoute>(
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<SetupBrowserAutofillRoute> {
SetupBrowserAutofillScreen()
}
}

View File

@ -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 = { },
)
}
}

View File

@ -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<SetupBrowserAutofillState, SetupBrowserAutofillEvent, SetupBrowserAutofillAction>(
// 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<BrowserAutofillSettingsOption>,
) : 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()
}
}

View File

@ -9,6 +9,7 @@ import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus 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.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.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@ -34,6 +35,7 @@ class SetupUnlockViewModel @Inject constructor(
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val biometricsEncryptionManager: BiometricsEncryptionManager, private val biometricsEncryptionManager: BiometricsEncryptionManager,
private val firstTimeActionManager: FirstTimeActionManager, private val firstTimeActionManager: FirstTimeActionManager,
private val browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
) : BaseViewModel<SetupUnlockState, SetupUnlockEvent, SetupUnlockAction>( ) : BaseViewModel<SetupUnlockState, SetupUnlockEvent, SetupUnlockAction>(
// We load the state from the savedStateHandle for testing purposes. // We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] ?: run { initialState = savedStateHandle[KEY_STATE] ?: run {
@ -203,10 +205,14 @@ class SetupUnlockViewModel @Inject constructor(
} }
private fun updateOnboardingStatusToNextStep() { private fun updateOnboardingStatusToNextStep() {
val nextStep = if (settingsRepository.isAutofillEnabledStateFlow.value) { val isAutofillEnabled = settingsRepository.isAutofillEnabledStateFlow.value
OnboardingStatus.FINAL_STEP val isBrowserAutofillUnconfigured = browserThirdPartyAutofillEnabledManager
} else { .browserThirdPartyAutofillStatus
OnboardingStatus.AUTOFILL_SETUP .isAnyIsAvailableAndDisabled
val nextStep = when {
!isAutofillEnabled -> OnboardingStatus.AUTOFILL_SETUP
isBrowserAutofillUnconfigured -> OnboardingStatus.BROWSER_AUTOFILL_SETUP
else -> OnboardingStatus.FINAL_STEP
} }
authRepository.setOnboardingStatus(nextStep) authRepository.setOnboardingStatus(nextStep)
} }

View File

@ -18,12 +18,15 @@ import com.bitwarden.ui.platform.theme.NonNullExitTransitionProvider
import com.bitwarden.ui.platform.theme.RootTransitionProviders import com.bitwarden.ui.platform.theme.RootTransitionProviders
import com.bitwarden.ui.platform.util.toObjectNavigationRoute import com.bitwarden.ui.platform.util.toObjectNavigationRoute
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupAutofillRoute 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.SetupCompleteRoute
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupUnlockRoute 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.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.navigateToSetupCompleteScreen
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupUnlockScreenAsRoot 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.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.setupCompleteDestination
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupUnlockDestinationAsRoot import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupUnlockDestinationAsRoot
import com.x8bit.bitwarden.ui.auth.feature.auth.AuthGraphRoute import com.x8bit.bitwarden.ui.auth.feature.auth.AuthGraphRoute
@ -107,6 +110,7 @@ fun RootNavScreen(
vaultUnlockDestination() vaultUnlockDestination()
vaultUnlockedGraph(navController) vaultUnlockedGraph(navController)
setupUnlockDestinationAsRoot() setupUnlockDestinationAsRoot()
setupBrowserAutofillDestination()
setupAutoFillDestinationAsRoot() setupAutoFillDestinationAsRoot()
setupCompleteDestination() setupCompleteDestination()
exportItemsGraph() exportItemsGraph()
@ -140,6 +144,7 @@ fun RootNavScreen(
RootNavState.OnboardingAccountLockSetup -> SetupUnlockRoute.AsRoot RootNavState.OnboardingAccountLockSetup -> SetupUnlockRoute.AsRoot
RootNavState.OnboardingAutoFillSetup -> SetupAutofillRoute.AsRoot RootNavState.OnboardingAutoFillSetup -> SetupAutofillRoute.AsRoot
RootNavState.OnboardingBrowserAutofillSetup -> SetupBrowserAutofillRoute
RootNavState.OnboardingStepsComplete -> SetupCompleteRoute RootNavState.OnboardingStepsComplete -> SetupCompleteRoute
} }
val currentRoute = navController.currentDestination?.rootLevelRoute() val currentRoute = navController.currentDestination?.rootLevelRoute()
@ -271,6 +276,10 @@ fun RootNavScreen(
navController.navigateToSetupAutoFillAsRootScreen(rootNavOptions) navController.navigateToSetupAutoFillAsRootScreen(rootNavOptions)
} }
RootNavState.OnboardingBrowserAutofillSetup -> {
navController.navigateToSetupBrowserAutofillScreen(rootNavOptions)
}
RootNavState.OnboardingStepsComplete -> { RootNavState.OnboardingStepsComplete -> {
navController.navigateToSetupCompleteScreen(rootNavOptions) navController.navigateToSetupCompleteScreen(rootNavOptions)
} }

View File

@ -99,15 +99,7 @@ class RootNavViewModel @Inject constructor(
userState.activeAccount.isVaultUnlocked && userState.activeAccount.isVaultUnlocked &&
userState.activeAccount.onboardingStatus != OnboardingStatus.COMPLETE -> { userState.activeAccount.onboardingStatus != OnboardingStatus.COMPLETE -> {
when (userState.activeAccount.onboardingStatus) { getOnboardingNavState(onboardingStatus = 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.")
}
} }
userState.activeAccount.isVaultUnlocked -> { userState.activeAccount.isVaultUnlocked -> {
@ -200,6 +192,19 @@ class RootNavViewModel @Inject constructor(
mutableStateFlow.update { updatedRootNavState } 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( private fun getRegistrationEventNavState(
registrationEvent: SpecialCircumstance.RegistrationEvent, registrationEvent: SpecialCircumstance.RegistrationEvent,
): RootNavState = when (registrationEvent) { ): RootNavState = when (registrationEvent) {
@ -402,6 +407,12 @@ sealed class RootNavState : Parcelable {
@Parcelize @Parcelize
data object OnboardingAutoFillSetup : RootNavState() 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. * App should show the onboarding steps complete screen.
*/ */

View File

@ -227,7 +227,10 @@ private fun AutoFillScreenContent(
BrowserAutofillSettingsCard( BrowserAutofillSettingsCard(
options = state.browserAutofillSettingsOptions, options = state.browserAutofillSettingsOptions,
onOptionClicked = autoFillHandlers.onBrowserAutofillSelected, 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)) Spacer(modifier = Modifier.height(8.dp))
} }

View File

@ -5,7 +5,6 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitwarden.core.util.isBuildVersionAtLeast import com.bitwarden.core.util.isBuildVersionAtLeast
import com.bitwarden.core.util.persistentListOfNotNull
import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text 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.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType 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.model.BrowserAutofillSettingsOption
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.util.toBrowserAutoFillSettingsOptions
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -303,23 +303,6 @@ enum class AutofillStyle(val label: Text) {
POPUP(label = BitwardenString.autofill_suggestions_popup.asText()), POPUP(label = BitwardenString.autofill_suggestions_popup.asText()),
} }
@Suppress("MaxLineLength")
private fun BrowserThirdPartyAutofillStatus.toBrowserAutoFillSettingsOptions(): ImmutableList<BrowserAutofillSettingsOption> =
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. * Models events for the auto-fill screen.
*/ */

View File

@ -6,14 +6,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.base.util.cardStyle import com.bitwarden.ui.platform.base.util.cardStyle
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin 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.model.CardStyle
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.model.BrowserAutofillSettingsOption 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 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 * @param onOptionClicked Lambda that is invoked when an option row is clicked and passes back the
* [BrowserPackage] for that option. * [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 @Composable
fun BrowserAutofillSettingsCard( fun BrowserAutofillSettingsCard(
options: ImmutableList<BrowserAutofillSettingsOption>, options: ImmutableList<BrowserAutofillSettingsOption>,
onOptionClicked: (BrowserPackage) -> Unit, onOptionClicked: (BrowserPackage) -> Unit,
enabled: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
supportingText: String? = null,
) { ) {
if (options.isEmpty()) return if (options.isEmpty()) return
Column(modifier = modifier) { Column(modifier = modifier) {
@ -45,37 +44,31 @@ fun BrowserAutofillSettingsCard(
onCheckedChange = { onCheckedChange = {
onOptionClicked(option.browserPackage) onOptionClicked(option.browserPackage)
}, },
cardStyle = if (index == 0) { cardStyle = when {
CardStyle.Top( supportingText == null -> options.toListItemCardStyle(index = index)
dividerPadding = 16.dp, index == 0 -> CardStyle.Top(dividerPadding = 16.dp)
) else -> CardStyle.Middle(dividerPadding = 16.dp)
} else {
CardStyle.Middle(
dividerPadding = 16.dp,
)
}, },
enabled = enabled,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.standardHorizontalMargin(), .standardHorizontalMargin(),
) )
} }
Text( supportingText?.let {
text = stringResource( Text(
id = BitwardenString text = it,
.improves_login_filling_for_supported_websites_on_selected_browsers, style = BitwardenTheme.typography.bodyMedium,
), color = BitwardenTheme.colorScheme.text.secondary,
style = BitwardenTheme.typography.bodyMedium, modifier = Modifier
color = BitwardenTheme.colorScheme.text.secondary, .fillMaxWidth()
modifier = Modifier .standardHorizontalMargin()
.fillMaxWidth() .cardStyle(
.standardHorizontalMargin() cardStyle = CardStyle.Bottom,
.cardStyle( paddingHorizontal = 16.dp,
cardStyle = CardStyle.Bottom, )
paddingHorizontal = 16.dp, .defaultMinSize(minHeight = 48.dp),
) )
.defaultMinSize(minHeight = 48.dp), }
)
} }
} }
@ -89,7 +82,6 @@ private fun ChromeAutofillSettingsCard_preview() {
BrowserAutofillSettingsOption.ChromeStable(enabled = false), BrowserAutofillSettingsOption.ChromeStable(enabled = false),
BrowserAutofillSettingsOption.ChromeBeta(enabled = true), BrowserAutofillSettingsOption.ChromeBeta(enabled = true),
), ),
enabled = true,
onOptionClicked = {}, onOptionClicked = {},
) )
} }

View File

@ -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<BrowserAutofillSettingsOption> =
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 },
)

View File

@ -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.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState 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.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@ -48,6 +49,11 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() {
every { userStateFlow } returns mutableUserStateFlow every { userStateFlow } returns mutableUserStateFlow
every { setOnboardingStatus(any()) } just runs every { setOnboardingStatus(any()) } just runs
} }
private val thirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager = mockk {
every {
browserThirdPartyAutofillStatus
} returns mockk { every { isAnyIsAvailableAndDisabled } returns false }
}
@BeforeEach @BeforeEach
fun setup() { fun setup() {
@ -131,10 +137,24 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() {
fun `handleTurnOnLaterConfirmClick sets onboarding status to FINAL_STEP`() { fun `handleTurnOnLaterConfirmClick sets onboarding status to FINAL_STEP`() {
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.trySendAction(SetupAutoFillAction.TurnOnLaterConfirmClick) viewModel.trySendAction(SetupAutoFillAction.TurnOnLaterConfirmClick)
verify { verify(exactly = 1) {
authRepository.setOnboardingStatus( authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP)
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() val viewModel = createViewModel()
viewModel.trySendAction(SetupAutoFillAction.ContinueClick) viewModel.trySendAction(SetupAutoFillAction.ContinueClick)
verify(exactly = 1) { verify(exactly = 1) {
authRepository.setOnboardingStatus( authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP)
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) firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = false)
} }
} }
@ -155,8 +188,9 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() {
@Test @Test
fun `handleContinueClick send NavigateBack event when not initial setup and sets first time flag to false`() = fun `handleContinueClick send NavigateBack event when not initial setup and sets first time flag to false`() =
runTest { runTest {
val viewModel = val viewModel = createViewModel(
createViewModel(initialState = DEFAULT_STATE.copy(isInitialSetup = false)) initialState = DEFAULT_STATE.copy(isInitialSetup = false),
)
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.trySendAction(SetupAutoFillAction.ContinueClick) viewModel.trySendAction(SetupAutoFillAction.ContinueClick)
assertEquals( assertEquals(
@ -168,9 +202,7 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() {
firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = false) firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = false)
} }
verify(exactly = 0) { verify(exactly = 0) {
authRepository.setOnboardingStatus( authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP)
OnboardingStatus.FINAL_STEP,
)
} }
} }
@ -204,6 +236,7 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() {
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
authRepository = authRepository, authRepository = authRepository,
firstTimeActionManager = firstTimeActionManager, firstTimeActionManager = firstTimeActionManager,
browserThirdPartyAutofillEnabledManager = thirdPartyAutofillEnabledManager,
) )
} }

View File

@ -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<IntentManager>()
private val mutableEventFlow = bufferedMutableSharedFlow<SetupBrowserAutofillEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val viewModel = mockk<SetupBrowserAutofillViewModel> {
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),
),
)

View File

@ -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),
),
)

View File

@ -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.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState 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.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
@ -59,6 +60,11 @@ class SetupUnlockViewModelTest : BaseViewModelTest() {
} returns false } returns false
every { createCipherOrNull(DEFAULT_USER_ID) } returns CIPHER every { createCipherOrNull(DEFAULT_USER_ID) } returns CIPHER
} }
private val thirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager = mockk {
every {
browserThirdPartyAutofillStatus
} returns mockk { every { isAnyIsAvailableAndDisabled } returns false }
}
@BeforeEach @BeforeEach
fun setup() { fun setup() {
@ -90,10 +96,9 @@ class SetupUnlockViewModelTest : BaseViewModelTest() {
fun `ContinueClick should call setOnboardingStatus and set to AUTOFILL_SETUP if AutoFill is not enabled`() { fun `ContinueClick should call setOnboardingStatus and set to AUTOFILL_SETUP if AutoFill is not enabled`() {
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.trySendAction(SetupUnlockAction.ContinueClick) viewModel.trySendAction(SetupUnlockAction.ContinueClick)
verify { verify(exactly = 1) {
authRepository.setOnboardingStatus( thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus
status = OnboardingStatus.AUTOFILL_SETUP, authRepository.setOnboardingStatus(status = OnboardingStatus.AUTOFILL_SETUP)
)
} }
} }
@ -131,29 +136,42 @@ class SetupUnlockViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @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 } mutableAutofillEnabledStateFlow.update { true }
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.trySendAction(SetupUnlockAction.ContinueClick) viewModel.trySendAction(SetupUnlockAction.ContinueClick)
verify(exactly = 1) { verify(exactly = 1) {
authRepository.setOnboardingStatus( thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus
status = OnboardingStatus.FINAL_STEP, authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP)
)
firstTimeActionManager.storeShowUnlockSettingBadge(showBadge = false) firstTimeActionManager.storeShowUnlockSettingBadge(showBadge = false)
} }
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @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 { runTest {
mutableAutofillEnabledStateFlow.update { true } mutableAutofillEnabledStateFlow.update { true }
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.trySendAction(SetupUnlockAction.SetUpLaterClick) viewModel.trySendAction(SetupUnlockAction.SetUpLaterClick)
verify { verify(exactly = 1) {
authRepository.setOnboardingStatus( thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus
status = OnboardingStatus.FINAL_STEP, authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP)
)
} }
} }
@ -395,6 +413,7 @@ class SetupUnlockViewModelTest : BaseViewModelTest() {
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
biometricsEncryptionManager = biometricsEncryptionManager, biometricsEncryptionManager = biometricsEncryptionManager,
firstTimeActionManager = firstTimeActionManager, firstTimeActionManager = firstTimeActionManager,
browserThirdPartyAutofillEnabledManager = thirdPartyAutofillEnabledManager,
) )
} }

View File

@ -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.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData 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.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.SetupCompleteRoute
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupUnlockRoute import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupUnlockRoute
import com.x8bit.bitwarden.ui.auth.feature.auth.AuthGraphRoute 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: // Make sure navigating to account setup complete works as expected:
rootNavStateFlow.value = RootNavState.OnboardingStepsComplete rootNavStateFlow.value = RootNavState.OnboardingStepsComplete
composeTestRule.runOnIdle { composeTestRule.runOnIdle {

View File

@ -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") @Suppress("MaxLineLength")
@Test @Test
fun `when the active user has an unlocked vault and they have a OnboardingStatus of FINAL_STEP the nav state should be OnboardingAutoFillSetup`() { fun `when the active user has an unlocked vault and they have a OnboardingStatus of FINAL_STEP the nav state should be OnboardingAutoFillSetup`() {

View File

@ -611,7 +611,7 @@ class AutoFillScreenTest : BitwardenComposeTest() {
.performClick() .performClick()
composeTestRule composeTestRule
.onNodeWithText("Use Chrome autofill integration (Beta)") .onNodeWithText("Use Chrome Beta autofill integration")
.performScrollTo() .performScrollTo()
.performClick() .performClick()

View File

@ -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,
)
}
}

View File

@ -758,6 +758,8 @@ Do you want to switch to this account?</string>
<string name="allow_bitwarden_authenticator_syncing">Allow authenticator syncing</string> <string name="allow_bitwarden_authenticator_syncing">Allow authenticator syncing</string>
<string name="there_was_an_issue_validating_the_registration_token">There was an issue validating the registration token.</string> <string name="there_was_an_issue_validating_the_registration_token">There was an issue validating the registration token.</string>
<string name="turn_on_autofill">Turn on autofill</string> <string name="turn_on_autofill">Turn on autofill</string>
<string name="turn_on_browser_autofill_integration">Turn on browser autofill integration</string>
<string name="youre_using_a_browser_that_requires_special_permissions">Youre using a browser that requires special permissions for Bitwarden to autofill your passwords. Enable your preferred autofill integration below.</string>
<string name="use_autofill_to_log_into_your_accounts">Use autofill to log into your accounts with a single tap.</string> <string name="use_autofill_to_log_into_your_accounts">Use autofill to log into your accounts with a single tap.</string>
<string name="turn_on_later">Turn on later</string> <string name="turn_on_later">Turn on later</string>
<string name="turn_on_autofill_later">Turn on autofill later?</string> <string name="turn_on_autofill_later">Turn on autofill later?</string>
@ -906,7 +908,7 @@ Do you want to switch to this account?</string>
<string name="self_host_server_url">Self-host server URL</string> <string name="self_host_server_url">Self-host server URL</string>
<string name="use_brave_autofill_integration">Use Brave autofill integration</string> <string name="use_brave_autofill_integration">Use Brave autofill integration</string>
<string name="use_chrome_autofill_integration">Use Chrome autofill integration</string> <string name="use_chrome_autofill_integration">Use Chrome autofill integration</string>
<string name="use_chrome_beta_autofill_integration">Use Chrome autofill integration (Beta)</string> <string name="use_chrome_beta_autofill_integration">Use Chrome Beta autofill integration</string>
<string name="improves_login_filling_for_supported_websites_on_selected_browsers">Improves login filling for supported websites on selected browsers. Once enabled, youll be directed to browser settings to enable third-party autofill.</string> <string name="improves_login_filling_for_supported_websites_on_selected_browsers">Improves login filling for supported websites on selected browsers. Once enabled, youll be directed to browser settings to enable third-party autofill.</string>
<string name="show_more">Show more</string> <string name="show_more">Show more</string>
<string name="no_folder">No folder</string> <string name="no_folder">No folder</string>