PM-24035: Add tooltip for website icons (#5554)

This commit is contained in:
David Perez 2025-07-22 15:06:54 -05:00 committed by GitHub
parent 3342ebf139
commit 9ed59e61a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 70 additions and 1 deletions

View File

@ -21,12 +21,14 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.navigation.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.model.CardStyle
import com.bitwarden.ui.platform.components.model.TooltipData
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
@ -35,7 +37,9 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.util.displayLabel
import kotlinx.collections.immutable.toImmutableList
@ -48,11 +52,15 @@ import kotlinx.collections.immutable.toImmutableList
fun AppearanceScreen(
onNavigateBack: () -> Unit,
viewModel: AppearanceViewModel = hiltViewModel(),
intentManager: IntentManager = LocalIntentManager.current,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
AppearanceEvent.NavigateBack -> onNavigateBack.invoke()
AppearanceEvent.NavigateToWebsiteIconsHelp -> {
intentManager.launchUri("https://bitwarden.com/help/website-icons/".toUri())
}
}
}
@ -134,6 +142,12 @@ fun AppearanceScreen(
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AppearanceAction.ShowWebsiteIconsToggle(it)) }
},
tooltip = TooltipData(
onClick = remember(viewModel) {
{ viewModel.trySendAction(AppearanceAction.ShowWebsiteIconsTooltipClick) }
},
contentDescription = stringResource(id = R.string.show_website_icons_help),
),
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("ShowWebsiteIconsSwitch")

View File

@ -22,6 +22,7 @@ private const val KEY_STATE = "state"
/**
* View model for the appearance screen.
*/
@Suppress("TooManyFunctions")
@HiltViewModel
class AppearanceViewModel @Inject constructor(
private val settingsRepository: SettingsRepository,
@ -58,6 +59,7 @@ class AppearanceViewModel @Inject constructor(
AppearanceAction.BackClick -> handleBackClicked()
is AppearanceAction.LanguageChange -> handleLanguageChanged(action)
is AppearanceAction.ShowWebsiteIconsToggle -> handleShowWebsiteIconsToggled(action)
AppearanceAction.ShowWebsiteIconsTooltipClick -> handleShowWebsiteIconsTooltipClick()
is AppearanceAction.ThemeChange -> handleThemeChanged(action)
is AppearanceAction.DynamicColorsToggle -> handleDynamicColorsToggled(action)
AppearanceAction.DismissDialog -> handleDismissDialog()
@ -68,6 +70,7 @@ class AppearanceViewModel @Inject constructor(
is AppearanceAction.Internal.AppLanguageStateUpdateReceive -> {
handleLanguageStateChange(action)
}
is AppearanceAction.Internal.DynamicColorsStateUpdateReceive -> {
handleDynamicColorsStateChange(action)
}
@ -106,6 +109,10 @@ class AppearanceViewModel @Inject constructor(
settingsRepository.isIconLoadingDisabled = !action.showWebsiteIcons
}
private fun handleShowWebsiteIconsTooltipClick() {
sendEvent(AppearanceEvent.NavigateToWebsiteIconsHelp)
}
private fun handleThemeChanged(action: AppearanceAction.ThemeChange) {
mutableStateFlow.update { it.copy(theme = action.theme) }
settingsRepository.appTheme = action.theme
@ -170,6 +177,11 @@ sealed class AppearanceEvent {
* Navigate back.
*/
data object NavigateBack : AppearanceEvent()
/**
* Navigate to the website icons help URL.
*/
data object NavigateToWebsiteIconsHelp : AppearanceEvent()
}
/**
@ -195,6 +207,11 @@ sealed class AppearanceAction {
val showWebsiteIcons: Boolean,
) : AppearanceAction()
/**
* User clicked the website icons tooltip.
*/
data object ShowWebsiteIconsTooltipClick : AppearanceAction()
/**
* Indicates that the user selected a new theme.
*/

View File

@ -265,6 +265,7 @@ Scanning will happen automatically.</string>
<string name="address">Address</string>
<string name="expiration">Expiration</string>
<string name="show_website_icons">Show website icons</string>
<string name="show_website_icons_help">Show website icons help</string>
<string name="show_website_icons_description">Show a recognizable image next to each login</string>
<string name="icons_url">Icons server URL</string>
<string name="vault_is_locked">Vault is locked</string>

View File

@ -10,15 +10,19 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.core.net.toUri
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.util.isBuildVersionAtLeast
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.util.assertNoDialogExists
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
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.coroutines.flow.MutableStateFlow
@ -37,12 +41,17 @@ class AppearanceScreenTest : BitwardenComposeTest() {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
}
private val intentManager: IntentManager = mockk {
every { launchUri(uri = any()) } just runs
}
@Before
fun setup() {
mockkStatic(::isBuildVersionAtLeast)
every { isBuildVersionAtLeast(any()) } returns true
setContent {
setContent(
intentManager = intentManager,
) {
AppearanceScreen(
onNavigateBack = { haveCalledNavigateBack = true },
viewModel = viewModel,
@ -172,12 +181,31 @@ class AppearanceScreenTest : BitwardenComposeTest() {
verify { viewModel.trySendAction(AppearanceAction.ShowWebsiteIconsToggle(true)) }
}
@Test
fun `on show website icons tooltip click should send ShowWebsiteIconsToggled`() {
composeTestRule
.onNodeWithContentDescription(label = "Show website icons help")
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(AppearanceAction.ShowWebsiteIconsTooltipClick)
}
}
@Test
fun `on NavigateBack should call onNavigateBack`() {
mutableEventFlow.tryEmit(AppearanceEvent.NavigateBack)
assertTrue(haveCalledNavigateBack)
}
@Test
fun `on NavigateToWebsiteIconsHelp should call launchUri`() {
mutableEventFlow.tryEmit(AppearanceEvent.NavigateToWebsiteIconsHelp)
verify {
intentManager.launchUri("https://bitwarden.com/help/website-icons/".toUri())
}
}
@Test
fun `dynamic colors should be displayed based on state`() {
composeTestRule.onNodeWithText("Dynamic colors")

View File

@ -136,6 +136,15 @@ class AppearanceViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `on ShowWebsiteIconsTooltipClick should emit NavigateToWebsiteIconsHelp`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AppearanceAction.ShowWebsiteIconsTooltipClick)
assertEquals(AppearanceEvent.NavigateToWebsiteIconsHelp, awaitItem())
}
}
@Test
fun `on ThemeChange should update state and set theme in SettingsRepository`() = runTest {
val viewModel = createViewModel()