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 c74c5f7d6e..35450c4769 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 @@ -199,31 +199,29 @@ private fun AutoFillScreenContent( .standardHorizontalMargin(), ) Spacer(modifier = Modifier.height(height = 8.dp)) - if (state.showInlineAutofillOption) { - BitwardenSwitch( - label = stringResource(id = R.string.inline_autofill), - supportingText = stringResource( - id = R.string.use_inline_autofill_explanation_long, - ), - isChecked = state.isUseInlineAutoFillEnabled, - onCheckedChange = autoFillHandlers.onUseInlineAutofillClick, - enabled = state.canInteractWithInlineAutofillToggle, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .testTag("InlineAutofillSwitch") - .standardHorizontalMargin(), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) + + AnimatedVisibility(visible = state.showInlineAutofill) { + Column { + FillStyleSelector( + selectedStyle = state.autofillStyle, + onStyleChange = autoFillHandlers.onAutofillStyleChange, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } } - if (state.browserAutofillSettingsOptions.isNotEmpty()) { - BrowserAutofillSettingsCard( - options = state.browserAutofillSettingsOptions, - onOptionClicked = autoFillHandlers.onBrowserAutofillSelected, - enabled = state.isAutoFillServicesEnabled, - ) - Spacer(modifier = Modifier.height(8.dp)) + AnimatedVisibility(visible = state.showBrowserSettingOptions) { + Column { + BrowserAutofillSettingsCard( + options = state.browserAutofillSettingsOptions, + onOptionClicked = autoFillHandlers.onBrowserAutofillSelected, + enabled = state.isAutoFillServicesEnabled, + ) + Spacer(modifier = Modifier.height(8.dp)) + } } if (state.showPasskeyManagementRow) { @@ -329,6 +327,26 @@ private fun AutoFillScreenContent( } } +@Composable +private fun FillStyleSelector( + selectedStyle: AutofillStyle, + onStyleChange: (AutofillStyle) -> Unit, + modifier: Modifier = Modifier, + resources: Resources = LocalContext.current.resources, +) { + BitwardenMultiSelectButton( + label = stringResource(id = R.string.display_autofill_suggestions), + supportingText = stringResource(id = R.string.use_inline_autofill_explanation_long), + options = AutofillStyle.entries.map { it.label() }.toImmutableList(), + selectedOption = selectedStyle.label(), + onOptionSelected = { + onStyleChange(AutofillStyle.entries.first { style -> style.label(resources) == it }) + }, + cardStyle = CardStyle.Full, + modifier = modifier.testTag(tag = "InlineAutofillSelector"), + ) +} + @Composable private fun AccessibilityAutofillSwitch( isAccessibilityAutoFillEnabled: Boolean, 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 2649617e76..3f04360d2d 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 @@ -8,6 +8,8 @@ import com.bitwarden.core.util.isBuildVersionAtLeast import com.bitwarden.core.util.persistentListOfNotNull import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asText +import com.x8bit.bitwarden.R 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 @@ -52,7 +54,11 @@ class AutoFillViewModel @Inject constructor( .value, isAutoFillServicesEnabled = settingsRepository.isAutofillEnabledStateFlow.value, isCopyTotpAutomaticallyEnabled = !settingsRepository.isAutoCopyTotpDisabled, - isUseInlineAutoFillEnabled = settingsRepository.isInlineAutofillEnabled, + autofillStyle = if (settingsRepository.isInlineAutofillEnabled) { + AutofillStyle.INLINE + } else { + AutofillStyle.POPUP + }, showInlineAutofillOption = isBuildVersionAtLeast(Build.VERSION_CODES.R), showPasskeyManagementRow = isBuildVersionAtLeast( Build.VERSION_CODES.UPSIDE_DOWN_CAKE, @@ -117,7 +123,7 @@ class AutoFillViewModel @Inject constructor( is AutoFillAction.DefaultUriMatchTypeSelect -> handleDefaultUriMatchTypeSelect(action) AutoFillAction.BlockAutoFillClick -> handleBlockAutoFillClick() AutoFillAction.UseAccessibilityAutofillClick -> handleUseAccessibilityAutofillClick() - is AutoFillAction.UseInlineAutofillClick -> handleUseInlineAutofillClick(action) + is AutoFillAction.AutofillStyleSelected -> handleAutofillStyleSelected(action) AutoFillAction.PasskeyManagementClick -> handlePasskeyManagementClick() is AutoFillAction.Internal -> handleInternalAction(action) AutoFillAction.AutofillActionCardCtaClick -> handleAutofillActionCardCtaClick() @@ -228,9 +234,9 @@ class AutoFillViewModel @Inject constructor( sendEvent(AutoFillEvent.NavigateToAccessibilitySettings) } - private fun handleUseInlineAutofillClick(action: AutoFillAction.UseInlineAutofillClick) { - settingsRepository.isInlineAutofillEnabled = action.isEnabled - mutableStateFlow.update { it.copy(isUseInlineAutoFillEnabled = action.isEnabled) } + private fun handleAutofillStyleSelected(action: AutoFillAction.AutofillStyleSelected) { + settingsRepository.isInlineAutofillEnabled = action.style == AutofillStyle.INLINE + mutableStateFlow.update { it.copy(autofillStyle = action.style) } } private fun handlePasskeyManagementClick() { @@ -281,7 +287,7 @@ data class AutoFillState( val isAccessibilityAutofillEnabled: Boolean, val isAutoFillServicesEnabled: Boolean, val isCopyTotpAutomaticallyEnabled: Boolean, - val isUseInlineAutoFillEnabled: Boolean, + val autofillStyle: AutofillStyle, val showInlineAutofillOption: Boolean, val showPasskeyManagementRow: Boolean, val defaultUriMatchType: UriMatchType, @@ -290,13 +296,31 @@ data class AutoFillState( val browserAutofillSettingsOptions: ImmutableList, val isUserManagedPrivilegedAppsEnabled: Boolean, ) : Parcelable { + /** + * Whether or not the dropdown controlling the [autofillStyle] value is displayed. + */ + val showInlineAutofill: Boolean get() = isAutoFillServicesEnabled && showInlineAutofillOption /** - * Whether or not the toggle controlling the [isUseInlineAutoFillEnabled] value can be - * interacted with. + * Whether or not the toggles for enabling 3rd-party autofill support should be displayed. */ - val canInteractWithInlineAutofillToggle: Boolean - get() = isAutoFillServicesEnabled + val showBrowserSettingOptions: Boolean + get() = isAutoFillServicesEnabled && browserAutofillSettingsOptions.isNotEmpty() +} + +/** + * The visual style of autofill that should be used. + */ +enum class AutofillStyle(val label: Text) { + /** + * Displays the autofill data in the keyboard. + */ + INLINE(label = R.string.autofill_suggestions_inline.asText()), + + /** + * Displays the autofill data as a popup attached to the field you are filling. + */ + POPUP(label = R.string.autofill_suggestions_popup.asText()), } @Suppress("MaxLineLength") @@ -425,8 +449,8 @@ sealed class AutoFillAction { /** * User clicked use inline autofill button. */ - data class UseInlineAutofillClick( - val isEnabled: Boolean, + data class AutofillStyleSelected( + val style: AutofillStyle, ) : AutoFillAction() /** diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/handlers/AutoFillHandlers.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/handlers/AutoFillHandlers.kt index 475fadea15..6aa7c119f7 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/handlers/AutoFillHandlers.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/handlers/AutoFillHandlers.kt @@ -4,6 +4,7 @@ import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.AutoFillAction import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.AutoFillViewModel +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.AutofillStyle /** * Handlers for the AutoFill screen. @@ -14,7 +15,7 @@ class AutoFillHandlers( val onAutofillActionCardClick: () -> Unit, val onAutofillActionCardDismissClick: () -> Unit, val onAutofillServicesClick: (isEnabled: Boolean) -> Unit, - val onUseInlineAutofillClick: (isEnabled: Boolean) -> Unit, + val onAutofillStyleChange: (style: AutofillStyle) -> Unit, val onBrowserAutofillSelected: (browserPackage: BrowserPackage) -> Unit, val onPasskeyManagementClick: () -> Unit, val onPrivilegedAppsClick: () -> Unit, @@ -47,12 +48,8 @@ class AutoFillHandlers( ), ) }, - onUseInlineAutofillClick = { - viewModel.trySendAction( - AutoFillAction.UseInlineAutofillClick( - it, - ), - ) + onAutofillStyleChange = { + viewModel.trySendAction(AutoFillAction.AutofillStyleSelected(it)) }, onBrowserAutofillSelected = { viewModel.trySendAction(AutoFillAction.BrowserAutofillSelected(it)) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 32f8d1045f..d87ba4fe7a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -425,7 +425,9 @@ Scanning will happen automatically. Privacy Policy Passkey management Autofill services - Use inline autofill + Display autofill suggestions + Inline (shows in keyboard) + Popup (shows over input field) Use accessibility Required to use the Autofill Quick-Action Tile. An organization policy is affecting your ownership options. @@ -660,8 +662,8 @@ Do you want to switch to this account? Session timeout action Account fingerprint phrase Use Bitwarden to save new passkeys and log in with passkeys stored in your vault. - The Android Autofill Framework is used to assist in filling login information into other apps on your device. - Use inline autofill if your selected keyboard supports it. Otherwise, use the default overlay. + Allow Bitwarden to use your saved login information to sign in to other apps on your device. + Choose how your autofill suggestions will appear when you sign in to other apps on your device. Additional options Continue to web app? Continue to %1$s? diff --git a/app/src/release/res/values/manifest.xml b/app/src/release/res/values/manifest.xml index 51ec05be25..14a10c8ce1 100644 --- a/app/src/release/res/values/manifest.xml +++ b/app/src/release/res/values/manifest.xml @@ -9,9 +9,8 @@ Authenticator App release variant: --> B06B54566AF2FBCC762700C8844B84EC410C230EACC3878FCF0248C0D9772A95 - - + + 52f393fb529fbf2ab5bb018bf17bf0f829d2fbebce099915aba42deab5a2fbb8 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 d4a4518839..1152cc0265 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 @@ -2,8 +2,6 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.autofill import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsEnabled -import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertIsOff import androidx.compose.ui.test.assertIsOn import androidx.compose.ui.test.filterToOne @@ -233,60 +231,85 @@ class AutoFillScreenTest : BitwardenComposeTest() { } @Test - fun `on use inline auto fill toggle should send UseInlineAutofillClick`() { + fun `on inline autofill style selected should send AutofillStyleSelected`() { mutableStateFlow.update { it.copy( isAutoFillServicesEnabled = true, - isUseInlineAutoFillEnabled = false, + autofillStyle = AutofillStyle.POPUP, ) } composeTestRule - .onNodeWithText("Use inline autofill") + .onNodeWithContentDescription( + label = "Popup (shows over input field). Display autofill suggestions. " + + "Choose how your autofill suggestions will appear when you sign in " + + "to other apps on your device.", + ) .performScrollTo() .performClick() - verify { viewModel.trySendAction(AutoFillAction.UseInlineAutofillClick(true)) } + + composeTestRule + .onNodeWithText(text = "Inline (shows in keyboard)") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + verify { + viewModel.trySendAction(AutoFillAction.AutofillStyleSelected(AutofillStyle.INLINE)) + } } @Test - fun `use inline autofill should be toggled on or off according to state`() { - composeTestRule - .onNodeWithText("Use inline autofill") - .performScrollTo() - .assertIsOff() - mutableStateFlow.update { it.copy(isUseInlineAutoFillEnabled = true) } - composeTestRule - .onNodeWithText("Use inline autofill") - .performScrollTo() - .assertIsOn() - } - - @Test - fun `use inline autofill should be disabled or enabled according to state`() { + fun `autofill style should be display selection according to state`() { mutableStateFlow.update { it.copy( isAutoFillServicesEnabled = true, - isUseInlineAutoFillEnabled = true, + autofillStyle = AutofillStyle.INLINE, ) } + composeTestRule + .onNodeWithContentDescription( + label = "Inline (shows in keyboard). Display autofill suggestions. " + + "Choose how your autofill suggestions will appear when you sign in " + + "to other apps on your device.", + ) + .performScrollTo() + .assertIsDisplayed() + mutableStateFlow.update { it.copy(autofillStyle = AutofillStyle.POPUP) } + composeTestRule + .onNodeWithContentDescription( + label = "Popup (shows over input field). Display autofill suggestions. " + + "Choose how your autofill suggestions will appear when you sign in " + + "to other apps on your device.", + ) + .performScrollTo() + .assertIsDisplayed() + } + + @Test + fun `use display autofill suggestions should be visible or enabled according to state`() { + mutableStateFlow.update { + it.copy(isAutoFillServicesEnabled = true) + } composeTestRule - .onNodeWithText("Use inline autofill") + .onNodeWithContentDescription( + label = "Inline (shows in keyboard). Display autofill suggestions. " + + "Choose how your autofill suggestions will appear when you sign in " + + "to other apps on your device.", + ) .performScrollTo() - .assertIsOn() - .assertIsEnabled() + .assertIsDisplayed() mutableStateFlow.update { - it.copy( - isAutoFillServicesEnabled = false, - isUseInlineAutoFillEnabled = true, - ) + it.copy(isAutoFillServicesEnabled = false) } composeTestRule - .onNodeWithText("Use inline autofill") - .performScrollTo() - .assertIsOn() - .assertIsNotEnabled() + .onNodeWithContentDescription( + label = "Inline (shows in keyboard). Display autofill suggestions. " + + "Choose how your autofill suggestions will appear when you sign in " + + "to other apps on your device.", + ) + .assertDoesNotExist() } @Suppress("MaxLineLength") @@ -319,11 +342,18 @@ class AutoFillScreenTest : BitwardenComposeTest() { @Test fun `use inline autofill should be displayed according to state`() { mutableStateFlow.update { - it.copy(showInlineAutofillOption = true) + it.copy( + isAutoFillServicesEnabled = true, + showInlineAutofillOption = true, + ) } composeTestRule - .onNodeWithText(text = "Use inline autofill") + .onNodeWithContentDescription( + label = "Inline (shows in keyboard). Display autofill suggestions. " + + "Choose how your autofill suggestions will appear when you sign in " + + "to other apps on your device.", + ) .performScrollTo() .assertIsDisplayed() @@ -331,7 +361,13 @@ class AutoFillScreenTest : BitwardenComposeTest() { it.copy(showInlineAutofillOption = false) } - composeTestRule.onNodeWithText(text = "Use inline autofill").assertDoesNotExist() + composeTestRule + .onNodeWithContentDescription( + label = "Inline (shows in keyboard). Display autofill suggestions. " + + "Choose how your autofill suggestions will appear when you sign in " + + "to other apps on your device.", + ) + .assertDoesNotExist() } @Test @@ -521,6 +557,7 @@ class AutoFillScreenTest : BitwardenComposeTest() { @Test fun `BrowserAutofillSettingsCard is only displayed when there are options in the list`() { + mutableStateFlow.update { it.copy(isAutoFillServicesEnabled = true) } val browserAutofillSupportingText = "Improves login filling for supported websites on selected browsers. " + "Once enabled, you’ll be directed to browser settings to enable " + @@ -656,7 +693,7 @@ private val DEFAULT_STATE: AutoFillState = AutoFillState( isAccessibilityAutofillEnabled = false, isAutoFillServicesEnabled = false, isCopyTotpAutomaticallyEnabled = false, - isUseInlineAutoFillEnabled = false, + autofillStyle = AutofillStyle.INLINE, showInlineAutofillOption = true, showPasskeyManagementRow = true, defaultUriMatchType = UriMatchType.DOMAIN, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt index e453c9bae2..1b20cf6c91 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt @@ -288,11 +288,12 @@ class AutoFillViewModelTest : BaseViewModelTest() { } @Test - fun `on UseInlineAutofillClick should update the state and save the new value to settings`() { + fun `on AutofillStyleSelected should update the state and save the new value to settings`() { val viewModel = createViewModel() - viewModel.trySendAction(AutoFillAction.UseInlineAutofillClick(false)) + val autofillStyle = AutofillStyle.POPUP + viewModel.trySendAction(AutoFillAction.AutofillStyleSelected(style = autofillStyle)) assertEquals( - DEFAULT_STATE.copy(isUseInlineAutoFillEnabled = false), + DEFAULT_STATE.copy(autofillStyle = autofillStyle), viewModel.stateFlow.value, ) verify { settingsRepository.isInlineAutofillEnabled = false } @@ -466,7 +467,7 @@ private val DEFAULT_STATE: AutoFillState = AutoFillState( isAccessibilityAutofillEnabled = false, isAutoFillServicesEnabled = false, isCopyTotpAutomaticallyEnabled = false, - isUseInlineAutoFillEnabled = true, + autofillStyle = AutofillStyle.INLINE, showInlineAutofillOption = false, showPasskeyManagementRow = true, defaultUriMatchType = UriMatchType.DOMAIN, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b37c08484d..b15f44eccb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,16 +15,16 @@ androdixAutofill = "1.3.0" androidxBiometrics = "1.2.0-alpha05" androidxBrowser = "1.8.0" androidxCamera = "1.4.2" -androidxComposeBom = "2025.05.01" +androidxComposeBom = "2025.06.01" androidxCore = "1.16.0" androidxCredentials = "1.5.0" androidxHiltNavigationCompose = "1.2.0" androidxLifecycle = "2.9.0" androidxNavigation = "2.9.0" -androidxRoom = "2.7.1" +androidxRoom = "2.7.2" androidxSecurityCrypto = "1.1.0-alpha06" androidxSplash = "1.1.0-rc01" -androidxWork = "2.10.1" +androidxWork = "2.10.2" bitwardenSdk = "1.0.0-20250623.141835-223" crashlytics = "3.0.4" detekt = "1.23.8" @@ -48,7 +48,7 @@ ksp = "2.2.0-2.0.2" mockk = "1.14.2" okhttp = "4.12.0" retrofitBom = "3.0.0" -robolectric = "4.14.1" +robolectric = "4.15.1" sonarqube = "6.2.0.5505" testng = "7.11.0" timber = "5.0.1"