PM-26187: Add autofill help call-to-action (#5942)

This commit is contained in:
David Perez authored and GitHub committed 2025-09-26 19:42:51 +00:00
1 parent 617be1fd95
commit eab2c17614
8 files changed
+158 -47

No files matched your search

@@ -140,7 +140,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
)
}
}
.onStart { emit(FirstTimeState()) }
.onStart { emit(currentOrDefaultUserFirstTimeState) }
.distinctUntilChanged()
override val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
@@ -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,
@@ -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<BrowserAutofillSettingsOption>,
) : 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()
@@ -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) },
)
}
}
@@ -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`() {
@@ -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()
@@ -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,
)
}
+2
View File
@@ -1123,4 +1123,6 @@ Do you want to switch to this account?</string>
<string name="select_account">Select account</string>
<string name="import_restricted_unable_to_import_credit_cards">Import restricted, unable to import cards from this account.</string>
<string name="verify_your_master_password">Verify your master password</string>
<string name="having_trouble_with_autofill">Having trouble with autofill?</string>
<string name="access_help_and_troubleshooting_documentation_here">Access help and troubleshooting documentation here</string>
</resources>