diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt index c4f3f3a479..88396527a3 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt @@ -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") diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceViewModel.kt index 2d8e96445c..950a7d10cb 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceViewModel.kt @@ -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. */ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6fa03a6ed0..fb6f63ec15 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -265,6 +265,7 @@ Scanning will happen automatically. Address Expiration Show website icons + Show website icons help Show a recognizable image next to each login Icons server URL Vault is locked diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreenTest.kt index 00b656f7f3..e6b9a32c94 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreenTest.kt @@ -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") diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceViewModelTest.kt index f9d940a17d..4fdfae618c 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceViewModelTest.kt @@ -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()