mirror of
https://github.com/bitwarden/android.git
synced 2026-10-22 02:53:19 -05:00
PM-26187: Add autofill help call-to-action (#5942)
This commit is contained in:
1 parent
617be1fd95
commit
eab2c17614
8 files changed
+158
-47
No files matched your search
+1
-1
@@ -140,7 +140,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
.onStart { emit(FirstTimeState()) }
|
||||
.onStart { emit(currentOrDefaultUserFirstTimeState) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
|
||||
|
||||
+80
-39
@@ -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,
|
||||
|
||||
+38
-3
@@ -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()
|
||||
|
||||
|
||||
+2
@@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
+21
@@ -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`() {
|
||||
|
||||
+10
@@ -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()
|
||||
|
||||
+4
-4
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user