From eab2c17614224ec8da64f48012a7dbce50d82747 Mon Sep 17 00:00:00 2001 From: David Perez Date: Fri, 26 Sep 2025 14:42:51 -0500 Subject: [PATCH] PM-26187: Add autofill help call-to-action (#5942) --- .../manager/FirstTimeActionManagerImpl.kt | 2 +- .../settings/autofill/AutoFillScreen.kt | 119 ++++++++++++------ .../settings/autofill/AutoFillViewModel.kt | 41 +++++- .../autofill/handlers/AutoFillHandlers.kt | 2 + .../settings/autofill/AutoFillScreenTest.kt | 21 ++++ .../autofill/AutoFillViewModelTest.kt | 10 ++ .../card/BitwardenActionCardSmall.kt | 8 +- ui/src/main/res/values/strings.xml | 2 + 8 files changed, 158 insertions(+), 47 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerImpl.kt index a128c39614..7164c3b426 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerImpl.kt @@ -140,7 +140,7 @@ class FirstTimeActionManagerImpl @Inject constructor( ) } } - .onStart { emit(FirstTimeState()) } + .onStart { emit(currentOrDefaultUserFirstTimeState) } .distinctUntilChanged() override val shouldShowAddLoginCoachMarkFlow: Flow 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 d236fa7d05..18f2d07b8b 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 @@ -1,7 +1,10 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.autofill import android.content.res.Resources +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -12,6 +15,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -39,6 +43,7 @@ import com.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.bitwarden.ui.platform.components.badge.NotificationBadge import com.bitwarden.ui.platform.components.card.BitwardenActionCard +import com.bitwarden.ui.platform.components.card.BitwardenActionCardSmall import com.bitwarden.ui.platform.components.card.actionCardExitAnimation import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog @@ -126,6 +131,12 @@ fun AutoFillScreen( AutoFillEvent.NavigateToLearnMore -> { intentManager.launchUri("https://bitwarden.com/help/uri-match-detection/".toUri()) } + + AutoFillEvent.NavigateToAutofillHelp -> { + intentManager.launchUri( + uri = "https://bitwarden.com/help/auto-fill-android-troubleshooting/".toUri(), + ) + } } } @@ -174,45 +185,14 @@ private fun AutoFillScreenContent( ) { Column(modifier = modifier) { Spacer(modifier = Modifier.height(height = 12.dp)) - AnimatedVisibility( - visible = state.showAutofillActionCard, - label = "AutofillActionCard", - exit = actionCardExitAnimation(), - ) { - BitwardenActionCard( - cardTitle = stringResource(BitwardenString.turn_on_autofill), - actionText = stringResource(BitwardenString.get_started), - onActionClick = autoFillHandlers.onAutofillActionCardClick, - onDismissClick = autoFillHandlers.onAutofillActionCardDismissClick, - leadingContent = { NotificationBadge(notificationCount = 1) }, - modifier = Modifier - .standardHorizontalMargin() - .padding(bottom = 16.dp), - ) - } - AnimatedVisibility( - visible = state.showBrowserAutofillActionCard, - label = "BrowserAutofillActionCard", - exit = actionCardExitAnimation(), - ) { - BitwardenActionCard( - cardTitle = stringResource( - id = BitwardenString.turn_on_browser_autofill_integration, - ), - cardSubtitle = pluralStringResource( - id = BitwardenPlurals - .youre_using_a_browser_that_requires_special_permissions_for_bitwarden, - count = state.browserCount, - ), - actionText = stringResource(id = BitwardenString.get_started), - onActionClick = autoFillHandlers.onBrowserAutofillActionCardClick, - onDismissClick = autoFillHandlers.onBrowserAutofillActionCardDismissClick, - leadingContent = { NotificationBadge(notificationCount = 1) }, - modifier = Modifier - .standardHorizontalMargin() - .padding(bottom = 16.dp), - ) - } + AutofillCallToActionCard( + state = state, + autoFillHandlers = autoFillHandlers, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = BitwardenString.autofill_title), modifier = Modifier @@ -364,6 +344,67 @@ private fun AutoFillScreenContent( } } +@Composable +private fun AutofillCallToActionCard( + state: AutoFillState, + autoFillHandlers: AutoFillHandlers, + modifier: Modifier, +) { + AnimatedContent( + targetState = state.ctaState, + label = "AutofillActionCard", + transitionSpec = { EnterTransition.None.togetherWith(actionCardExitAnimation()) }, + modifier = modifier, + ) { + when (it) { + CtaState.AUTOFILL -> { + BitwardenActionCard( + cardTitle = stringResource(id = BitwardenString.turn_on_autofill), + actionText = stringResource(id = BitwardenString.get_started), + onActionClick = autoFillHandlers.onAutofillActionCardClick, + onDismissClick = autoFillHandlers.onAutofillActionCardDismissClick, + leadingContent = { NotificationBadge(notificationCount = 1) }, + ) + } + + CtaState.BROWSER_AUTOFILL -> { + BitwardenActionCard( + cardTitle = stringResource( + id = BitwardenString.turn_on_browser_autofill_integration, + ), + cardSubtitle = pluralStringResource( + id = BitwardenPlurals + .youre_using_a_browser_that_requires_special_permissions_for_bitwarden, + count = state.browserCount, + ), + actionText = stringResource(id = BitwardenString.get_started), + onActionClick = autoFillHandlers.onBrowserAutofillActionCardClick, + onDismissClick = autoFillHandlers.onBrowserAutofillActionCardDismissClick, + leadingContent = { NotificationBadge(notificationCount = 1) }, + ) + } + + CtaState.DEFAULT -> { + BitwardenActionCardSmall( + actionIcon = rememberVectorPainter(id = BitwardenDrawable.ic_question_circle), + actionText = stringResource(id = BitwardenString.having_trouble_with_autofill), + callToActionText = stringResource( + id = BitwardenString.access_help_and_troubleshooting_documentation_here, + ), + onCardClicked = autoFillHandlers.onHelpCardClick, + trailingContent = { + Icon( + painter = rememberVectorPainter(BitwardenDrawable.ic_chevron_right), + contentDescription = null, + tint = BitwardenTheme.colorScheme.icon.primary, + ) + }, + ) + } + } + } +} + @Composable private fun FillStyleSelector( selectedStyle: AutofillStyle, 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 26c5e2f5c0..2861dbd3be 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 @@ -45,6 +45,7 @@ class AutoFillViewModel @Inject constructor( initialState = savedStateHandle[KEY_STATE] ?: run { val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId + val firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState AutoFillState( isAskToAddLoginEnabled = !settingsRepository.isAutofillSavePromptDisabled, isAccessibilityAutofillEnabled = settingsRepository @@ -62,8 +63,8 @@ class AutoFillViewModel @Inject constructor( Build.VERSION_CODES.UPSIDE_DOWN_CAKE, ), defaultUriMatchType = settingsRepository.defaultUriMatchType, - showAutofillActionCard = false, - showBrowserAutofillActionCard = false, + showAutofillActionCard = firstTimeState.showSetupAutofillCard, + showBrowserAutofillActionCard = firstTimeState.showSetupBrowserAutofillCard, activeUserId = userId, browserAutofillSettingsOptions = browserThirdPartyAutofillEnabledManager .browserThirdPartyAutofillStatus @@ -132,6 +133,7 @@ class AutoFillViewModel @Inject constructor( AutoFillAction.AboutPrivilegedAppsClick -> handleAboutPrivilegedAppsClick() AutoFillAction.PrivilegedAppsClick -> handlePrivilegedAppsClick() AutoFillAction.LearnMoreClick -> handleLearnMoreClick() + AutoFillAction.HelpCardClick -> handleHelpCardClick() } private fun handlePrivilegedAppsClick() { @@ -142,6 +144,10 @@ class AutoFillViewModel @Inject constructor( sendEvent(AutoFillEvent.NavigateToLearnMore) } + private fun handleHelpCardClick() { + sendEvent(AutoFillEvent.NavigateToAutofillHelp) + } + private fun handleInternalAction(action: AutoFillAction.Internal) { when (action) { is AutoFillAction.Internal.AccessibilityEnabledUpdateReceive -> { @@ -300,6 +306,16 @@ data class AutoFillState( val activeUserId: String, val browserAutofillSettingsOptions: ImmutableList, ) : Parcelable { + /** + * Indicates which call-to-action that should be displayed. + */ + val ctaState: CtaState + get() = when { + showAutofillActionCard -> CtaState.AUTOFILL + showBrowserAutofillActionCard -> CtaState.BROWSER_AUTOFILL + else -> CtaState.DEFAULT + } + /** * Whether or not the dropdown controlling the [autofillStyle] value is displayed. */ @@ -317,6 +333,15 @@ data class AutoFillState( get() = isAutoFillServicesEnabled && browserAutofillSettingsOptions.isNotEmpty() } +/** + * A representation of which call-to-action that should be displayed. + */ +enum class CtaState { + AUTOFILL, + BROWSER_AUTOFILL, + DEFAULT, +} + /** * The visual style of autofill that should be used. */ @@ -392,6 +417,11 @@ sealed class AutoFillEvent { * Navigate to the learn more. */ data object NavigateToLearnMore : AutoFillEvent() + + /** + * Navigate to the autofill help page. + */ + data object NavigateToAutofillHelp : AutoFillEvent() } /** @@ -493,6 +523,11 @@ sealed class AutoFillAction { */ data object LearnMoreClick : AutoFillAction() + /** + * User has clicked the help CTA. + */ + data object HelpCardClick : AutoFillAction() + /** * Internal actions. */ @@ -512,7 +547,7 @@ sealed class AutoFillAction { ) : Internal() /** - * An update for changes in the [showAutofillActionCard] value from the settings repository. + * An update for changes in the [firstTimeState] value from the settings repository. */ data class UpdateShowAutofillActionCard(val firstTimeState: FirstTimeState) : Internal() 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 6d78cd985e..c305164006 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 @@ -28,6 +28,7 @@ class AutoFillHandlers( val onDefaultUriMatchTypeSelect: (defaultUriMatchType: UriMatchType) -> Unit, val onBlockAutoFillClick: () -> Unit, val onLearnMoreClick: () -> Unit, + val onHelpCardClick: () -> Unit, ) { @Suppress("UndocumentedPublicClass") companion object { @@ -82,6 +83,7 @@ class AutoFillHandlers( }, onBlockAutoFillClick = { viewModel.trySendAction(AutoFillAction.BlockAutoFillClick) }, onLearnMoreClick = { viewModel.trySendAction(AutoFillAction.LearnMoreClick) }, + onHelpCardClick = { viewModel.trySendAction(AutoFillAction.HelpCardClick) }, ) } } 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 14de851be3..dea0269474 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 @@ -103,6 +103,16 @@ class AutoFillScreenTest : BitwardenComposeTest() { ) } + @Test + fun `on NavigateToAutofillHelp should launch the browser to the autofill help page`() { + mutableEventFlow.tryEmit(AutoFillEvent.NavigateToAutofillHelp) + verify(exactly = 1) { + intentManager.launchUri( + uri = "https://bitwarden.com/help/auto-fill-android-troubleshooting/".toUri(), + ) + } + } + @Test fun `on NavigateToAccessibilitySettings should attempt to navigate to system settings`() { mutableEventFlow.tryEmit(AutoFillEvent.NavigateToAccessibilitySettings) @@ -159,6 +169,17 @@ class AutoFillScreenTest : BitwardenComposeTest() { composeTestRule.assertNoDialogExists() } + @Test + fun `on help card CTA should send HelpCardClick`() { + composeTestRule + .onNodeWithText(text = "Having trouble with autofill?") + .performClick() + + verify(exactly = 1) { + viewModel.trySendAction(AutoFillAction.HelpCardClick) + } + } + @Suppress("MaxLineLength") @Test fun `on use accessibility click with accessibility already enabled should emit UseAccessibilityAutofillClick`() { 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 710ce61bbd..4337edf33b 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 @@ -43,6 +43,7 @@ class AutoFillViewModelTest : BaseViewModelTest() { private val mutableFirstTimeStateFlow = MutableStateFlow(FirstTimeState()) private val firstTimeActionManager: FirstTimeActionManager = mockk { every { firstTimeStateFlow } returns mutableFirstTimeStateFlow + every { currentOrDefaultUserFirstTimeState } answers { mutableFirstTimeStateFlow.value } every { storeShowAutoFillSettingBadge(showBadge = any()) } just runs every { storeShowBrowserAutofillSettingBadge(showBadge = any()) } just runs } @@ -177,6 +178,15 @@ class AutoFillViewModelTest : BaseViewModelTest() { verify { settingsRepository.isAutofillSavePromptDisabled = false } } + @Test + fun `on HelpCardClick should emit NavigateToAutofillHelp`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(AutoFillAction.HelpCardClick) + assertEquals(AutoFillEvent.NavigateToAutofillHelp, awaitItem()) + } + } + @Test fun `on AutoFillServicesClick with false should disable autofill`() { val viewModel = createViewModel() diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/card/BitwardenActionCardSmall.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/card/BitwardenActionCardSmall.kt index 669ae671ff..0cff52403e 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/card/BitwardenActionCardSmall.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/card/BitwardenActionCardSmall.kt @@ -36,10 +36,10 @@ fun BitwardenActionCardSmall( actionIcon: VectorPainter, actionText: String, callToActionText: String, - callToActionTextColor: Color = BitwardenTheme.colorScheme.text.primary, - colors: CardColors = bitwardenCardColors(), onCardClicked: () -> Unit, modifier: Modifier = Modifier, + callToActionTextColor: Color = BitwardenTheme.colorScheme.text.primary, + colors: CardColors = bitwardenCardColors(), trailingContent: (@Composable BoxScope.() -> Unit)? = null, ) { Card( @@ -67,13 +67,13 @@ fun BitwardenActionCardSmall( ) { Text( text = actionText, - style = BitwardenTheme.typography.bodyLarge, + style = BitwardenTheme.typography.titleMedium, color = BitwardenTheme.colorScheme.text.primary, ) Spacer(modifier = Modifier.height(8.dp)) Text( text = callToActionText, - style = BitwardenTheme.typography.labelLarge, + style = BitwardenTheme.typography.bodyMedium, color = callToActionTextColor, ) } diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 6954be1516..0d75671587 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -1123,4 +1123,6 @@ Do you want to switch to this account? Select account Import restricted, unable to import cards from this account. Verify your master password + Having trouble with autofill? + Access help and troubleshooting documentation here