From 3dde47d0c64e534bba83834b1b9888909e0be22a Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Tue, 11 Mar 2025 15:55:42 -0400 Subject: [PATCH 1/5] [PM-19108] Add privileged app management screen Allow users to manage trusted privileged applications and view privileged applications that are trusted by external sources. --- .../autofill/fido2/di/Fido2ProviderModule.kt | 6 +- .../autofill/fido2/model/PrivilegedAppData.kt | 10 + .../repository/PrivilegedAppRepository.kt | 32 +- .../repository/PrivilegedAppRepositoryImpl.kt | 130 ++++- .../feature/settings/SettingsNavigation.kt | 4 + .../settings/autofill/AutoFillNavigation.kt | 2 + .../settings/autofill/AutoFillScreen.kt | 47 +- .../settings/autofill/AutoFillViewModel.kt | 15 + .../PrivilegedAppsListNavigation.kt | 28 ++ .../PrivilegedAppsListScreen.kt | 456 ++++++++++++++++++ .../PrivilegedAppsViewModel.kt | 304 ++++++++++++ .../model/PrivilegedAppListItem.kt | 16 + app/src/main/res/values/strings.xml | 10 + .../settings/autofill/AutoFillScreenTest.kt | 4 + 14 files changed, 1042 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/PrivilegedAppData.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsListNavigation.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsListScreen.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsViewModel.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/model/PrivilegedAppListItem.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/di/Fido2ProviderModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/di/Fido2ProviderModule.kt index 3670e420a81..eebc1270f45 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/di/Fido2ProviderModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/di/Fido2ProviderModule.kt @@ -87,12 +87,16 @@ object Fido2ProviderModule { @Provides @Singleton - fun provideFido2PrivilegedAppRepository( + fun providePrivilegedAppRepository( fido2PrivilegedAppDiskSource: Fido2PrivilegedAppDiskSource, + assetManager: AssetManager, + dispatcherManager: DispatcherManager, json: Json, ): PrivilegedAppRepository = PrivilegedAppRepositoryImpl( fido2PrivilegedAppDiskSource = fido2PrivilegedAppDiskSource, + assetManager = assetManager, + dispatcherManager = dispatcherManager, json = json, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/PrivilegedAppData.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/PrivilegedAppData.kt new file mode 100644 index 00000000000..4aa338d4666 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/PrivilegedAppData.kt @@ -0,0 +1,10 @@ +package com.x8bit.bitwarden.data.autofill.fido2.model + +/** + * Represents privileged applications that are trusted by various sources. + */ +data class PrivilegedAppData( + val googleTrustedApps: PrivilegedAppAllowListJson, + val communityTrustedApps: PrivilegedAppAllowListJson, + val userTrustedApps: PrivilegedAppAllowListJson, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/repository/PrivilegedAppRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/repository/PrivilegedAppRepository.kt index a17c838fb3f..3cedacd405f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/repository/PrivilegedAppRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/repository/PrivilegedAppRepository.kt @@ -1,27 +1,49 @@ package com.x8bit.bitwarden.data.autofill.fido2.repository import com.x8bit.bitwarden.data.autofill.fido2.model.PrivilegedAppAllowListJson -import kotlinx.coroutines.flow.Flow +import com.x8bit.bitwarden.data.autofill.fido2.model.PrivilegedAppData +import com.x8bit.bitwarden.data.platform.repository.model.DataState +import kotlinx.coroutines.flow.StateFlow /** * Repository for managing privileged apps trusted by the user. */ interface PrivilegedAppRepository { + /** + * Flow that represents the trusted privileged apps data. + */ + val trustedAppDataStateFlow: StateFlow> + /** * Flow of the user's trusted privileged apps. */ - val userTrustedPrivilegedAppsFlow: Flow + val userTrustedAppsFlow: StateFlow> + + /** + * Flow of the Google's trusted privileged apps. + */ + val googleTrustedPrivilegedAppsFlow: StateFlow> + + /** + * Flow of the community's trusted privileged apps. + */ + val communityTrustedAppsFlow: StateFlow> /** * List the user's trusted privileged apps. */ - suspend fun getAllUserTrustedPrivilegedApps(): PrivilegedAppAllowListJson + suspend fun getUserTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson? + + /** + * List Google's trusted privileged apps. + */ + suspend fun getGoogleTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson? /** - * Returns true if the given [packageName] and [signature] are trusted. + * List community's trusted privileged apps. */ - suspend fun isPrivilegedAppAllowed(packageName: String, signature: String): Boolean + suspend fun getCommunityTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson? /** * Adds the given [packageName] and [signature] to the list of trusted privileged apps. diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/repository/PrivilegedAppRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/repository/PrivilegedAppRepositoryImpl.kt index 40151c8dacf..a30c505e685 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/repository/PrivilegedAppRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/repository/PrivilegedAppRepositoryImpl.kt @@ -3,10 +3,33 @@ package com.x8bit.bitwarden.data.autofill.fido2.repository import com.x8bit.bitwarden.data.autofill.fido2.datasource.disk.Fido2PrivilegedAppDiskSource import com.x8bit.bitwarden.data.autofill.fido2.datasource.disk.entity.Fido2PrivilegedAppInfoEntity import com.x8bit.bitwarden.data.autofill.fido2.model.PrivilegedAppAllowListJson -import kotlinx.coroutines.flow.Flow +import com.x8bit.bitwarden.data.autofill.fido2.model.PrivilegedAppData +import com.x8bit.bitwarden.data.platform.manager.AssetManager +import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager +import com.x8bit.bitwarden.data.platform.repository.model.DataState +import com.x8bit.bitwarden.data.platform.repository.util.combineDataStates +import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json +/** + * A "stop timeout delay" in milliseconds used to let a shared coroutine continue to run for the + * specified period of time after it no longer has subscribers. + */ +private const val STOP_TIMEOUT_DELAY_MS: Long = 1000L +private const val GOOGLE_ALLOW_LIST_FILE_NAME = "fido2_privileged_google.json" +private const val COMMUNITY_ALLOW_LIST_FILE_NAME = "fido2_privileged_community.json" private const val ANDROID_TYPE = "android" private const val RELEASE_BUILD = "release" @@ -15,25 +38,104 @@ private const val RELEASE_BUILD = "release" */ class PrivilegedAppRepositoryImpl( private val fido2PrivilegedAppDiskSource: Fido2PrivilegedAppDiskSource, + private val assetManager: AssetManager, + dispatcherManager: DispatcherManager, private val json: Json, ) : PrivilegedAppRepository { - override val userTrustedPrivilegedAppsFlow: Flow = - fido2PrivilegedAppDiskSource.userTrustedPrivilegedAppsFlow - .map { it.toFido2PrivilegedAppAllowListJson() } + private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined) + private val ioScope = CoroutineScope(dispatcherManager.io) + + private val mutableUserTrustedAppsFlow = + MutableStateFlow>(DataState.Loading) + private val mutableGoogleTrustedAppsFlow = + MutableStateFlow>(DataState.Loading) + private val mutableCommunityTrustedPrivilegedAppsFlow = + MutableStateFlow>(DataState.Loading) + + override val trustedAppDataStateFlow: StateFlow> = + combine( + userTrustedAppsFlow, + googleTrustedPrivilegedAppsFlow, + communityTrustedAppsFlow, + ) { userAppsState, googleAppsState, communityAppsState -> + combineDataStates( + userAppsState, + googleAppsState, + communityAppsState, + ) { userApps, googleApps, communityApps -> + PrivilegedAppData( + googleTrustedApps = googleApps, + communityTrustedApps = communityApps, + userTrustedApps = userApps, + ) + } + } + .stateIn( + scope = unconfinedScope, + started = SharingStarted.WhileSubscribed(stopTimeoutMillis = STOP_TIMEOUT_DELAY_MS), + initialValue = DataState.Loading, + ) + + override val userTrustedAppsFlow: StateFlow> + get() = mutableUserTrustedAppsFlow.asStateFlow() + + override val googleTrustedPrivilegedAppsFlow: StateFlow> + get() = mutableGoogleTrustedAppsFlow.asStateFlow() + + override val communityTrustedAppsFlow: StateFlow> + get() = mutableCommunityTrustedPrivilegedAppsFlow.asStateFlow() + + init { + ioScope.launch { + val googleAppsDataState = assetManager.readAsset(fileName = GOOGLE_ALLOW_LIST_FILE_NAME) + .map { json.decodeFromString(it) } + .fold( + onSuccess = { DataState.Loaded(it) }, + onFailure = { DataState.Error(it) }, + ) + + val communityAppsDataState = + assetManager.readAsset(fileName = COMMUNITY_ALLOW_LIST_FILE_NAME) + .map { json.decodeFromString(it) } + .fold( + onSuccess = { DataState.Loaded(it) }, + onFailure = { DataState.Error(it) }, + ) - override suspend fun getAllUserTrustedPrivilegedApps(): PrivilegedAppAllowListJson = - fido2PrivilegedAppDiskSource.getAllUserTrustedPrivilegedApps() + mutableGoogleTrustedAppsFlow.value = googleAppsDataState + mutableCommunityTrustedPrivilegedAppsFlow.value = communityAppsDataState + + fido2PrivilegedAppDiskSource.userTrustedPrivilegedAppsFlow + .map { DataState.Loaded(it.toFido2PrivilegedAppAllowListJson()) } + .onEach { + mutableUserTrustedAppsFlow.value = it + } + .launchIn(ioScope) + } + } + + override suspend fun getUserTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson = + fido2PrivilegedAppDiskSource + .getAllUserTrustedPrivilegedApps() .toFido2PrivilegedAppAllowListJson() - override suspend fun isPrivilegedAppAllowed( - packageName: String, - signature: String, - ): Boolean = fido2PrivilegedAppDiskSource - .isPrivilegedAppTrustedByUser( - packageName = packageName, - signature = signature, - ) + override suspend fun getGoogleTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson? = + withContext(ioScope.coroutineContext) { + assetManager + .readAsset(fileName = GOOGLE_ALLOW_LIST_FILE_NAME) + .map { json.decodeFromStringOrNull(it) } + .getOrNull() + } + + override suspend fun getCommunityTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson? { + return withContext(ioScope.coroutineContext) { + assetManager + .readAsset(fileName = COMMUNITY_ALLOW_LIST_FILE_NAME) + .map { json.decodeFromStringOrNull(it) } + .getOrNull() + } + } override suspend fun addTrustedPrivilegedApp( packageName: String, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt index 370a6a19b4e..1febe792539 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt @@ -14,6 +14,8 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.navigateToApp import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.autoFillDestination import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.blockautofill.blockAutoFillDestination import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.blockautofill.navigateToBlockAutoFillScreen +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist.privilegedAppsListDestination +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist.navigateToPrivilegedAppsList import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.navigateToAutoFill import com.x8bit.bitwarden.ui.platform.feature.settings.other.navigateToOther import com.x8bit.bitwarden.ui.platform.feature.settings.other.otherDestination @@ -66,6 +68,7 @@ fun NavGraphBuilder.settingsGraph( onNavigateBack = { navController.popBackStack() }, onNavigateToBlockAutoFillScreen = { navController.navigateToBlockAutoFillScreen() }, onNavigateToSetupAutofill = onNavigateToSetupAutoFillScreen, + onNavigateToTrustedAppsScreen = { navController.navigateToPrivilegedAppsList() }, ) otherDestination(onNavigateBack = { navController.popBackStack() }) vaultSettingsDestination( @@ -75,6 +78,7 @@ fun NavGraphBuilder.settingsGraph( onNavigateToImportLogins = onNavigateToImportLogins, ) blockAutoFillDestination(onNavigateBack = { navController.popBackStack() }) + privilegedAppsListDestination(onNavigateBack = { navController.popBackStack() }) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt index fc3bf7aa5c0..1c700d43107 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt @@ -14,6 +14,7 @@ fun NavGraphBuilder.autoFillDestination( onNavigateBack: () -> Unit, onNavigateToBlockAutoFillScreen: () -> Unit, onNavigateToSetupAutofill: () -> Unit, + onNavigateToTrustedAppsScreen: () -> Unit, ) { composableWithPushTransitions( route = AUTO_FILL_ROUTE, @@ -22,6 +23,7 @@ fun NavGraphBuilder.autoFillDestination( onNavigateBack = onNavigateBack, onNavigateToBlockAutoFillScreen = onNavigateToBlockAutoFillScreen, onNavigateToSetupAutofill = onNavigateToSetupAutofill, + onNavigateToPrivilegedAppsScreen = onNavigateToTrustedAppsScreen, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt index 13b5b729da5..9b41a812b30 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt @@ -9,9 +9,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -31,6 +33,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.Text +import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.platform.base.util.mirrorIfRtl import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.badge.NotificationBadge @@ -50,6 +55,7 @@ import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.chrome.ChromeAutofillSettingsCard import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.util.displayLabel import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager +import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import kotlinx.collections.immutable.toImmutableList /** @@ -64,6 +70,7 @@ fun AutoFillScreen( intentManager: IntentManager = LocalIntentManager.current, onNavigateToBlockAutoFillScreen: () -> Unit, onNavigateToSetupAutofill: () -> Unit, + onNavigateToPrivilegedAppsScreen: () -> Unit, ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() val context = LocalContext.current @@ -101,6 +108,9 @@ fun AutoFillScreen( releaseChannel = event.releaseChannel, ) } + AutoFillEvent.NavigateToPrivilegedApps -> { + onNavigateToPrivilegedAppsScreen() + } } } @@ -230,12 +240,21 @@ fun AutoFillScreen( id = R.string.set_bitwarden_as_passkey_manager_description, ), withDivider = false, - cardStyle = CardStyle.Full, + cardStyle = CardStyle.Top(hasDivider = true), modifier = Modifier .fillMaxWidth() .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(height = 8.dp)) + PrivilegedAppsRow( + text = R.string.privileged_apps.asText(), + onClick = remember(viewModel) { + { viewModel.trySendAction(AutoFillAction.TrustedAppsClick) } + }, + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(8.dp)) } AccessibilityAutofillSwitch( isAccessibilityAutoFillEnabled = state.isAccessibilityAutofillEnabled, @@ -373,3 +392,27 @@ private fun DefaultUriMatchTypeRow( modifier = modifier, ) } + +@Composable +private fun PrivilegedAppsRow( + text: Text, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + BitwardenTextRow( + text = text(), + description = stringResource(R.string.privileged_apps_description), + onClick = onClick, + cardStyle = CardStyle.Bottom, + modifier = modifier, + ) { + Icon( + painter = rememberVectorPainter(id = R.drawable.ic_chevron_right), + contentDescription = null, + tint = BitwardenTheme.colorScheme.icon.primary, + modifier = Modifier + .mirrorIfRtl() + .size(size = 16.dp), + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt index 164b682597f..42999063396 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt @@ -112,6 +112,7 @@ class AutoFillViewModel @Inject constructor( AutoFillAction.AutofillActionCardCtaClick -> handleAutofillActionCardCtaClick() AutoFillAction.DismissShowAutofillActionCard -> handleDismissShowAutofillActionCard() is AutoFillAction.ChromeAutofillSelected -> handleChromeAutofillSelected(action) + AutoFillAction.TrustedAppsClick -> handleTrustedAppsClick() } private fun handleInternalAction(action: AutoFillAction.Internal) { @@ -150,6 +151,10 @@ class AutoFillViewModel @Inject constructor( sendEvent(AutoFillEvent.NavigateToChromeAutofillSettings(action.releaseChannel)) } + private fun handleTrustedAppsClick() { + sendEvent(AutoFillEvent.NavigateToPrivilegedApps) + } + private fun handleDismissShowAutofillActionCard() { dismissShowAutofillActionCard() } @@ -323,6 +328,11 @@ sealed class AutoFillEvent { * Navigates to the setup autofill screen. */ data object NavigateToSetupAutofill : AutoFillEvent() + + /** + * Navigates to the privileged apps screen. + */ + object NavigateToPrivilegedApps : AutoFillEvent() } /** @@ -394,6 +404,11 @@ sealed class AutoFillAction { */ data object AutofillActionCardCtaClick : AutoFillAction() + /** + * User has clicked the trusted apps action card. + */ + object TrustedAppsClick : AutoFillAction() + /** * User has clicked one of the chrome autofill options. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsListNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsListNavigation.kt new file mode 100644 index 00000000000..646a19d2c0d --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsListNavigation.kt @@ -0,0 +1,28 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions + +private const val PRIVILEGED_APPS_LIST_ROUTE = "settings_privileged_apps" + +/** + * Add privileged apps list destination to the nav graph. + */ +fun NavGraphBuilder.privilegedAppsListDestination( + onNavigateBack: () -> Unit, +) { + composableWithPushTransitions( + route = PRIVILEGED_APPS_LIST_ROUTE, + ) { + PrivilegedAppsListScreen(onNavigateBack = onNavigateBack) + } +} + +/** + * Navigate to the privileged apps list screen. + */ +fun NavController.navigateToPrivilegedAppsList(navOptions: NavOptions? = null) { + navigate(PRIVILEGED_APPS_LIST_ROUTE, navOptions) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsListScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsListScreen.kt new file mode 100644 index 00000000000..838cd022842 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsListScreen.kt @@ -0,0 +1,456 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.cardStyle +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle +import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton +import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog +import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog +import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog +import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle +import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold +import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter +import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist.model.PrivilegedAppListItem +import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager +import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme +import kotlinx.collections.immutable.toImmutableList + +/** + * Top level composable for the privileged apps list. + */ +@Suppress("LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PrivilegedAppsListScreen( + onNavigateBack: () -> Unit, + intentManager: IntentManager = LocalIntentManager.current, + viewModel: PrivilegedAppsViewModel = hiltViewModel(), +) { + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + EventsEffect(viewModel) { event -> + when (event) { + PrivilegedAppsListEvent.NavigateBack -> onNavigateBack() + is PrivilegedAppsListEvent.NavigateToUri -> { + intentManager.launchUri(uri = event.uri) + } + } + } + + when (val dialogState = state.dialogState) { + is Fido2TrustState.DialogState.Loading -> { + BitwardenLoadingDialog(stringResource(R.string.loading)) + } + + is Fido2TrustState.DialogState.ConfirmLaunchUri -> { + BitwardenTwoButtonDialog( + title = dialogState.title.invoke(), + message = dialogState.message.invoke(), + confirmButtonText = stringResource(R.string.continue_text), + dismissButtonText = stringResource(R.string.cancel), + onConfirmClick = remember { + { + viewModel.trySendAction(PrivilegedAppsListAction.DismissDialogClick) + intentManager.launchUri(dialogState.uri) + } + }, + onDismissClick = remember { + { viewModel.trySendAction(PrivilegedAppsListAction.DismissDialogClick) } + }, + onDismissRequest = remember { + { viewModel.trySendAction(PrivilegedAppsListAction.DismissDialogClick) } + }, + ) + } + + is Fido2TrustState.DialogState.ConfirmDeleteTrustedApp -> { + BitwardenTwoButtonDialog( + title = stringResource(R.string.delete), + message = stringResource( + R.string.are_you_sure_you_want_to_stop_trusting_x, + dialogState.app.packageName, + ), + confirmButtonText = stringResource(R.string.ok), + dismissButtonText = stringResource(R.string.cancel), + onConfirmClick = remember(viewModel) { + { + viewModel.trySendAction( + PrivilegedAppsListAction.UserTrustedAppDeleteConfirmClick( + app = dialogState.app, + ), + ) + } + }, + onDismissClick = remember(viewModel) { + { viewModel.trySendAction(PrivilegedAppsListAction.DismissDialogClick) } + }, + onDismissRequest = remember(viewModel) { + { viewModel.trySendAction(PrivilegedAppsListAction.DismissDialogClick) } + }, + ) + } + + is Fido2TrustState.DialogState.General -> { + BitwardenBasicDialog( + title = stringResource(R.string.an_error_has_occurred), + message = dialogState.message.invoke(), + onDismissRequest = remember(viewModel) { + { viewModel.trySendAction(PrivilegedAppsListAction.DismissDialogClick) } + }, + ) + } + + null -> Unit + } + + BitwardenScaffold( + topBar = { + BitwardenTopAppBar( + title = stringResource(R.string.privileged_apps), + scrollBehavior = scrollBehavior, + navigationIcon = rememberVectorPainter(id = R.drawable.ic_back), + navigationIconContentDescription = stringResource(id = R.string.back), + onNavigationIconClick = remember(viewModel) { + { viewModel.trySendAction(PrivilegedAppsListAction.BackClick) } + }, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_external_link, + contentDescription = stringResource(R.string.bitwarden_help_center), + onClick = remember(viewModel) { + { + viewModel.trySendAction( + action = PrivilegedAppsListAction.LaunchHelpCenterClick, + ) + } + }, + ) + }, + ) + }, + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + ) { + PrivilegedAppsListContent( + state = state, + onDeleteClick = remember(viewModel) { + { viewModel.trySendAction(PrivilegedAppsListAction.UserTrustedAppDeleteClick(it)) } + }, + modifier = Modifier.fillMaxSize(), + ) + } +} + +@Suppress("LongMethod") +@Composable +private fun PrivilegedAppsListContent( + state: Fido2TrustState, + onDeleteClick: (PrivilegedAppListItem) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + ) { + if (state.userTrustedApps.isNotEmpty()) { + item(key = "trusted_by_you") { + Spacer(modifier = Modifier.height(12.dp)) + PrivilegedAppHeaderItem( + headerText = stringResource(R.string.trusted_by_you), + learnMoreText = stringResource(R.string.trusted_by_you_learn_more), + itemCount = state.userTrustedApps.size, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(8.dp)) + } + itemsIndexed( + key = { _, item -> "userTrust_${item.packageName}_${item.signature}" }, + items = state.userTrustedApps, + ) { index, item -> + PrivilegedAppListItem( + item = item, + canDelete = true, + onClick = remember(item) { + { onDeleteClick(item) } + }, + cardStyle = state.userTrustedApps + .toListItemCardStyle(index = index), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .animateItem(), + ) + } + } + + if (state.communityTrustedApps.isNotEmpty()) { + item(key = "trusted_by_community") { + Spacer(modifier = Modifier.height(12.dp)) + PrivilegedAppHeaderItem( + headerText = stringResource(R.string.trusted_by_the_community), + learnMoreText = stringResource(R.string.trusted_by_community_learn_more), + itemCount = state.communityTrustedApps.size, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(8.dp)) + } + itemsIndexed( + key = { _, item -> "communityTrust_${item.packageName}_${item.signature}" }, + items = state.communityTrustedApps, + ) { index, item -> + PrivilegedAppListItem( + item = item, + canDelete = false, + onClick = {}, + cardStyle = state.communityTrustedApps + .toListItemCardStyle(index = index), + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .animateItem(), + ) + } + } + + if (state.googleTrustedApps.isNotEmpty()) { + item(key = "trusted_by_google") { + Spacer(modifier = Modifier.height(12.dp)) + PrivilegedAppHeaderItem( + headerText = stringResource(R.string.trusted_by_google), + learnMoreText = stringResource(R.string.trusted_by_google_learn_more), + itemCount = state.googleTrustedApps.size, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(8.dp)) + } + itemsIndexed( + key = { _, item -> "googleTrust_${item.packageName}_${item.signature}" }, + items = state.googleTrustedApps, + ) { index, item -> + PrivilegedAppListItem( + item = item, + canDelete = false, + onClick = { }, + cardStyle = state.googleTrustedApps + .toListItemCardStyle(index), + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .animateItem(), + ) + } + } + + item { + Spacer(modifier = Modifier.height(88.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) + } + } +} + +@Composable +private fun LazyItemScope.PrivilegedAppHeaderItem( + headerText: String, + learnMoreText: String, + itemCount: Int, + modifier: Modifier = Modifier, +) { + var showDialog by rememberSaveable { mutableStateOf(false) } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .standardHorizontalMargin(), + ) { + BitwardenListHeaderText( + label = headerText, + supportingLabel = itemCount.toString(), + modifier = Modifier.animateItem(), + ) + val size by animateDpAsState( + targetValue = 16.dp, + label = "${headerText}_animation", + ) + Spacer(modifier = Modifier.width(width = 8.dp)) + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_question_circle_small, + contentDescription = "", + onClick = { showDialog = !showDialog }, + contentColor = BitwardenTheme.colorScheme.icon.secondary, + modifier = Modifier.size(size), + ) + } + + if (showDialog) { + BitwardenBasicDialog( + title = headerText, + message = learnMoreText, + onDismissRequest = { showDialog = false }, + ) + } +} + +@Composable +private fun LazyItemScope.PrivilegedAppListItem( + item: PrivilegedAppListItem, + canDelete: Boolean, + onClick: () -> Unit, + cardStyle: CardStyle, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardStyle( + cardStyle = cardStyle, + paddingStart = 16.dp, + paddingEnd = if (canDelete) 4.dp else 16.dp, + ), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + modifier = Modifier, + text = item.packageName, + style = BitwardenTheme.typography.bodyLarge, + color = BitwardenTheme.colorScheme.text.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = item.signature, + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + if (canDelete) { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_send_pending_delete, + contentDescription = "", + onClick = onClick, + ) + } + } +} + +@Preview +@Composable +private fun PrivilegedAppsListScreenPreview() { + PrivilegedAppsListContent( + state = Fido2TrustState( + googleTrustedApps = listOf( + PrivilegedAppListItem( + packageName = "com.x8bit.bitwarden.google", + signature = "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX", + ), + PrivilegedAppListItem( + packageName = "com.bitwarden.authenticator.google", + signature = "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX", + ), + PrivilegedAppListItem( + packageName = "com.google.android.apps.walletnfcrel.google", + signature = "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX", + ), + ) + .toImmutableList(), + communityTrustedApps = listOf( + PrivilegedAppListItem( + packageName = "com.x8bit.bitwarden.community", + signature = "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX", + ), + PrivilegedAppListItem( + packageName = "com.bitwarden.authenticator.community", + signature = "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX", + ), + PrivilegedAppListItem( + packageName = "com.google.android.apps.walletnfcrel.community", + signature = "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX", + ), + ) + .toImmutableList(), + userTrustedApps = listOf( + PrivilegedAppListItem( + packageName = "com.x8bit.bitwarden.you", + signature = "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX", + ), + PrivilegedAppListItem( + packageName = "com.bitwarden.authenticator.you", + signature = "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX", + ), + PrivilegedAppListItem( + packageName = "com.google.android.apps.walletnfcrel.you", + signature = "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX", + ), + ) + .toImmutableList(), + dialogState = null, + ), + onDeleteClick = {}, + ) +} + +@Preview(showBackground = true) +@Composable +private fun PrivilegedAppListItemPreview() { + LazyColumn { + item { + PrivilegedAppListItem( + item = PrivilegedAppListItem( + packageName = "com.google.android.apps.walletnfcrel", + signature = "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX", + ), + canDelete = false, + onClick = {}, + cardStyle = CardStyle.Middle(hasDivider = false), + ) + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsViewModel.kt new file mode 100644 index 00000000000..16a0bcdaf36 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsViewModel.kt @@ -0,0 +1,304 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist + +import android.net.Uri +import android.os.Parcelable +import androidx.core.net.toUri +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.autofill.fido2.model.PrivilegedAppAllowListJson +import com.x8bit.bitwarden.data.autofill.fido2.model.PrivilegedAppData +import com.x8bit.bitwarden.data.autofill.fido2.repository.PrivilegedAppRepository +import com.x8bit.bitwarden.data.platform.repository.model.DataState +import com.x8bit.bitwarden.ui.platform.base.BaseViewModel +import com.x8bit.bitwarden.ui.platform.base.util.Text +import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist.model.PrivilegedAppListItem +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +private const val KEY_STATE = "state" +private val BITWARDEN_HELP_CENTER_USING_PASSKEYS_URI = + "https://bitwarden.com/help/storing-passkeys/#using-passkeys-with-bitwarden".toUri() + +/** + * View model for the [PrivilegedAppsListScreen]. + */ +@HiltViewModel +class PrivilegedAppsViewModel @Inject constructor( + private val privilegedAppRepository: PrivilegedAppRepository, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = savedStateHandle[KEY_STATE] + ?: Fido2TrustState( + googleTrustedApps = emptyList() + .toImmutableList(), + communityTrustedApps = emptyList() + .toImmutableList(), + userTrustedApps = emptyList() + .toImmutableList(), + dialogState = null, + ), +) { + + init { + privilegedAppRepository + .trustedAppDataStateFlow + .map { PrivilegedAppsListAction.Internal.PrivilegedAppDataStateReceive(it) } + .onEach(::handleAction) + .launchIn(viewModelScope) + } + + override fun handleAction(action: PrivilegedAppsListAction) { + when (action) { + is PrivilegedAppsListAction.UserTrustedAppDeleteClick -> { + handleUserTrustedAppDeleteClick(action.app) + } + + is PrivilegedAppsListAction.UserTrustedAppDeleteConfirmClick -> { + handleUserTrustedAppDeleteConfirmClick(action.app) + } + + is PrivilegedAppsListAction.DismissDialogClick -> { + mutableStateFlow.update { it.copy(dialogState = null) } + } + + is PrivilegedAppsListAction.LaunchHelpCenterClick -> { + mutableStateFlow.update { + it.copy( + dialogState = Fido2TrustState.DialogState.ConfirmLaunchUri( + title = R.string.continue_to_help_center.asText(), + message = R.string.learn_more_about_using_passkeys_with_bitwarden + .asText(), + uri = BITWARDEN_HELP_CENTER_USING_PASSKEYS_URI, + ), + ) + } + } + + is PrivilegedAppsListAction.BackClick -> sendEvent(PrivilegedAppsListEvent.NavigateBack) + + is PrivilegedAppsListAction.Internal.PrivilegedAppDataStateReceive -> { + handleTrustedAppDataStateReceive(action.dataState) + } + } + } + + private fun handleTrustedAppDataStateReceive(dataState: DataState) { + when (dataState) { + is DataState.Loaded -> handleTrustedAppDataStateLoaded(dataState) + DataState.Loading -> handleTrustedAppDataStateLoading() + is DataState.Pending -> handleTrustedAppDataStatePending(dataState) + is DataState.Error -> handleTrustedAppDataStateError() + // Network connection is not required so we ignore NoNetwork state. + is DataState.NoNetwork -> Unit + } + } + + private fun handleTrustedAppDataStateError() { + mutableStateFlow.update { + it.copy( + dialogState = Fido2TrustState.DialogState.General( + message = R.string.generic_error_message.asText(), + ), + ) + } + } + + private fun handleTrustedAppDataStateLoaded( + loaded: DataState.Loaded, + ) { + mutableStateFlow.update { + it.copy( + googleTrustedApps = loaded.data + .googleTrustedApps + .toImmutablePrivilegedAppList(), + communityTrustedApps = loaded.data + .communityTrustedApps + .toImmutablePrivilegedAppList(), + userTrustedApps = loaded.data + .userTrustedApps + .toImmutablePrivilegedAppList(), + dialogState = null, + ) + } + } + + private fun handleTrustedAppDataStateLoading() { + mutableStateFlow.update { it.copy(dialogState = Fido2TrustState.DialogState.Loading) } + } + + private fun handleTrustedAppDataStatePending( + state: DataState.Pending, + ) { + mutableStateFlow.update { + it.copy( + googleTrustedApps = state.data + .googleTrustedApps + .toImmutablePrivilegedAppList(), + communityTrustedApps = state.data + .communityTrustedApps + .toImmutablePrivilegedAppList(), + userTrustedApps = state.data + .userTrustedApps + .toImmutablePrivilegedAppList(), + dialogState = Fido2TrustState.DialogState.Loading, + ) + } + } + + private fun handleUserTrustedAppDeleteClick(app: PrivilegedAppListItem) { + mutableStateFlow.update { + it.copy( + dialogState = Fido2TrustState.DialogState.ConfirmDeleteTrustedApp(app), + ) + } + } + + private fun handleUserTrustedAppDeleteConfirmClick(app: PrivilegedAppListItem) { + mutableStateFlow.update { + it.copy( + dialogState = Fido2TrustState.DialogState.Loading, + ) + } + viewModelScope.launch { + privilegedAppRepository + .removeTrustedPrivilegedApp( + packageName = app.packageName, + signature = app.signature, + ) + } + } + + private fun PrivilegedAppAllowListJson.toImmutablePrivilegedAppList() = this.apps + .map { it.toPrivilegedAppListItem() } + .toImmutableList() + + private fun PrivilegedAppAllowListJson.PrivilegedAppJson.toPrivilegedAppListItem() = + PrivilegedAppListItem( + packageName = info.packageName, + signature = info.signatures + .first() + .certFingerprintSha256, + ) +} + +/** + * Models the state of the [PrivilegedAppsViewModel]. + */ +@Parcelize +data class Fido2TrustState( + val googleTrustedApps: ImmutableList, + val communityTrustedApps: ImmutableList, + val userTrustedApps: ImmutableList, + val dialogState: DialogState?, +) : Parcelable { + + /** + * Models the different dialog states that the [PrivilegedAppsViewModel] may be in. + */ + sealed class DialogState : Parcelable { + + /** + * Show the loading dialog. + */ + @Parcelize + data object Loading : DialogState() + + /** + * Show the confirm delete trusted app dialog. + */ + @Parcelize + data class ConfirmDeleteTrustedApp( + val app: PrivilegedAppListItem, + ) : DialogState() + + /** + * Show a general dialog. + */ + @Parcelize + data class General( + val message: Text, + ) : DialogState() + + /** + * Show the confirm launch URI dialog. + */ + @Parcelize + data class ConfirmLaunchUri( + val title: Text, + val message: Text, + val uri: Uri, + ) : DialogState() + } +} + +/** + * Models events that the [PrivilegedAppsViewModel] may send. + */ +sealed class PrivilegedAppsListEvent { + + /** + * Navigate back to the previous screen. + */ + data object NavigateBack : PrivilegedAppsListEvent() + + /** + * Navigate to the given [uri]. + */ + data class NavigateToUri(val uri: Uri) : PrivilegedAppsListEvent() +} + +/** + * Models actions that the [PrivilegedAppsViewModel] may receive. + */ +sealed class PrivilegedAppsListAction { + /** + * Navigate back to the previous screen. + */ + data object BackClick : PrivilegedAppsListAction() + + /** + * The user has dismissed the current dialog. + */ + data object DismissDialogClick : PrivilegedAppsListAction() + + /** + * The user has clicked the help center button. + */ + data object LaunchHelpCenterClick : PrivilegedAppsListAction() + + /** + * The user has selected to delete a trusted app from their local trust store. + */ + data class UserTrustedAppDeleteClick( + val app: PrivilegedAppListItem, + ) : PrivilegedAppsListAction() + + /** + * The user has confirmed that they want to delete a trusted app from their local trust store. + */ + data class UserTrustedAppDeleteConfirmClick( + val app: PrivilegedAppListItem, + ) : PrivilegedAppsListAction() + + /** + * Models actions that the [PrivilegedAppsViewModel] itself may send. + */ + sealed class Internal : PrivilegedAppsListAction() { + /** + * Indicates that the trusted app data state has been received. + */ + data class PrivilegedAppDataStateReceive( + val dataState: DataState, + ) : Internal() + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/model/PrivilegedAppListItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/model/PrivilegedAppListItem.kt new file mode 100644 index 00000000000..d888c329770 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/model/PrivilegedAppListItem.kt @@ -0,0 +1,16 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Represents a single item in the list of trusted privileged apps. + * + * @param packageName The package name of the privileged app. + * @param signature The signature of the privileged app. + */ +@Parcelize +data class PrivilegedAppListItem( + val packageName: String, + val signature: String, +) : Parcelable diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 137b89819eb..e760c22c94d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1232,4 +1232,14 @@ Do you want to switch to this account? Trust Unknown Application not trusted + Privileged apps + Trusted by Google + Trusted by you + Trusted by the community + Are you sure you want to stop trusting %s? + Learn more about using passkeys with Bitwarden. + These are applications YOU trust to perform passkey operations on behalf of other parties. These are often web browsers, and are not currently trusted by Google or the Bitwarden community. + These are applications the Bitwarden Community Members trust to perform passkey operations on behalf of other parties. These are often web browsers, and are not distributed through Google\'s Play Store. They are used and considered safe by our community members. + These are applications Google considers safe to perform passkey operations on behalf of other parties. These are often web browsers, and are available in Google\'s Play Store.\n + Applications, often web browsers, trusted to perform passkey operations on behalf of other parties. diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt index dfe37be0c34..0ab401de94f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt @@ -39,6 +39,7 @@ class AutoFillScreenTest : BaseComposeTest() { private var onNavigateBackCalled = false private var onNavigateToBlockAutoFillScreenCalled = false private var onNavigateToSetupAutoFillScreenCalled = false + private var onNavigateToPrivilegedAppsScreenCalled = false private val mutableEventFlow = bufferedMutableSharedFlow() private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) @@ -62,6 +63,9 @@ class AutoFillScreenTest : BaseComposeTest() { onNavigateBack = { onNavigateBackCalled = true }, onNavigateToBlockAutoFillScreen = { onNavigateToBlockAutoFillScreenCalled = true }, onNavigateToSetupAutofill = { onNavigateToSetupAutoFillScreenCalled = true }, + onNavigateToPrivilegedAppsScreen = { + onNavigateToPrivilegedAppsScreenCalled = true + }, viewModel = viewModel, ) } From 5fdad28cdb1da60bf362ff00d914ea628d58fea2 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Tue, 18 Mar 2025 18:22:26 -0400 Subject: [PATCH 2/5] Update UI based on feedback from Design --- .../feature/settings/SettingsNavigation.kt | 10 +- .../settings/autofill/AutoFillNavigation.kt | 2 + .../settings/autofill/AutoFillScreen.kt | 406 ++++++++++-------- .../settings/autofill/AutoFillViewModel.kt | 21 +- .../autofill/handlers/AutoFillHandlers.kt | 93 ++++ .../about/AboutPrivilegedAppsNavigation.kt | 32 ++ .../about/AboutPrivilegedAppsScreen.kt | 98 +++++ .../list}/PrivilegedAppsListNavigation.kt | 2 +- .../list}/PrivilegedAppsListScreen.kt | 289 ++++--------- .../list/PrivilegedAppsListViewModel.kt} | 72 +--- .../list}/model/PrivilegedAppListItem.kt | 2 +- app/src/main/res/values/strings.xml | 19 +- 12 files changed, 590 insertions(+), 456 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/handlers/AutoFillHandlers.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/about/AboutPrivilegedAppsNavigation.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/about/AboutPrivilegedAppsScreen.kt rename app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/{privilegedappslist => privilegedapps/list}/PrivilegedAppsListNavigation.kt (97%) rename app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/{privilegedappslist => privilegedapps/list}/PrivilegedAppsListScreen.kt (56%) rename app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/{privilegedappslist/PrivilegedAppsViewModel.kt => privilegedapps/list/PrivilegedAppsListViewModel.kt} (80%) rename app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/{privilegedappslist => privilegedapps/list}/model/PrivilegedAppListItem.kt (94%) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt index 1febe792539..54ff332b8ca 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt @@ -14,9 +14,11 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.navigateToApp import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.autoFillDestination import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.blockautofill.blockAutoFillDestination import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.blockautofill.navigateToBlockAutoFillScreen -import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist.privilegedAppsListDestination -import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist.navigateToPrivilegedAppsList import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.navigateToAutoFill +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedapps.about.aboutPrivilegedAppsDestination +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedapps.about.navigateToAboutPrivilegedAppsScreen +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedapps.list.navigateToPrivilegedAppsList +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedapps.list.privilegedAppsListDestination import com.x8bit.bitwarden.ui.platform.feature.settings.other.navigateToOther import com.x8bit.bitwarden.ui.platform.feature.settings.other.otherDestination import com.x8bit.bitwarden.ui.platform.feature.settings.vault.navigateToVaultSettings @@ -69,6 +71,9 @@ fun NavGraphBuilder.settingsGraph( onNavigateToBlockAutoFillScreen = { navController.navigateToBlockAutoFillScreen() }, onNavigateToSetupAutofill = onNavigateToSetupAutoFillScreen, onNavigateToTrustedAppsScreen = { navController.navigateToPrivilegedAppsList() }, + onNavigateToAboutPrivilegedAppsScreen = { + navController.navigateToAboutPrivilegedAppsScreen() + }, ) otherDestination(onNavigateBack = { navController.popBackStack() }) vaultSettingsDestination( @@ -79,6 +84,7 @@ fun NavGraphBuilder.settingsGraph( ) blockAutoFillDestination(onNavigateBack = { navController.popBackStack() }) privilegedAppsListDestination(onNavigateBack = { navController.popBackStack() }) + aboutPrivilegedAppsDestination(navigateBack = { navController.popBackStack() }) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt index 1c700d43107..700f6216295 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt @@ -15,6 +15,7 @@ fun NavGraphBuilder.autoFillDestination( onNavigateToBlockAutoFillScreen: () -> Unit, onNavigateToSetupAutofill: () -> Unit, onNavigateToTrustedAppsScreen: () -> Unit, + onNavigateToAboutPrivilegedAppsScreen: () -> Unit, ) { composableWithPushTransitions( route = AUTO_FILL_ROUTE, @@ -24,6 +25,7 @@ fun NavGraphBuilder.autoFillDestination( onNavigateToBlockAutoFillScreen = onNavigateToBlockAutoFillScreen, onNavigateToSetupAutofill = onNavigateToSetupAutofill, onNavigateToPrivilegedAppsScreen = onNavigateToTrustedAppsScreen, + onNavigateToAboutPrivilegedAppsScreen = onNavigateToAboutPrivilegedAppsScreen, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt index 9b41a812b30..4b587a1589d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt @@ -3,17 +3,21 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.autofill import android.content.res.Resources import android.widget.Toast import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width 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 import androidx.compose.runtime.Composable @@ -22,11 +26,14 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -35,10 +42,11 @@ import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.asText -import com.x8bit.bitwarden.ui.platform.base.util.mirrorIfRtl +import com.x8bit.bitwarden.ui.platform.base.util.cardStyle import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.badge.NotificationBadge +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard import com.x8bit.bitwarden.ui.platform.components.card.actionCardExitAnimation import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog @@ -53,9 +61,11 @@ import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.chrome.ChromeAutofillSettingsCard +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.handlers.AutoFillHandlers import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.util.displayLabel import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList /** @@ -71,6 +81,7 @@ fun AutoFillScreen( onNavigateToBlockAutoFillScreen: () -> Unit, onNavigateToSetupAutofill: () -> Unit, onNavigateToPrivilegedAppsScreen: () -> Unit, + onNavigateToAboutPrivilegedAppsScreen: () -> Unit, ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() val context = LocalContext.current @@ -111,6 +122,9 @@ fun AutoFillScreen( AutoFillEvent.NavigateToPrivilegedApps -> { onNavigateToPrivilegedAppsScreen() } + AutoFillEvent.NavigateToAboutPrivilegedApps -> { + onNavigateToAboutPrivilegedAppsScreen() + } } } @@ -123,6 +137,7 @@ fun AutoFillScreen( } val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val autoFillHandlers = remember(viewModel) { AutoFillHandlers.create(viewModel) } BitwardenScaffold( modifier = Modifier .fillMaxSize() @@ -139,195 +154,178 @@ fun AutoFillScreen( ) }, ) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), + AutoFillContent( + state = state, + autoFillHandlers = autoFillHandlers, + ) + } +} + +@Suppress("LongMethod") +@Composable +private fun AutoFillContent( + state: AutoFillState, + autoFillHandlers: AutoFillHandlers, +) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { + Spacer(modifier = Modifier.height(height = 12.dp)) + AnimatedVisibility( + visible = state.showAutofillActionCard, + label = "AutofillActionCard", + exit = actionCardExitAnimation(), ) { - Spacer(modifier = Modifier.height(height = 12.dp)) - AnimatedVisibility( - visible = state.showAutofillActionCard, - label = "AutofillActionCard", - exit = actionCardExitAnimation(), - ) { - BitwardenActionCard( - cardTitle = stringResource(R.string.turn_on_autofill), - actionText = stringResource(R.string.get_started), - onActionClick = remember(viewModel) { - { - viewModel.trySendAction(AutoFillAction.AutofillActionCardCtaClick) - } - }, - onDismissClick = remember(viewModel) { - { - viewModel.trySendAction(AutoFillAction.DismissShowAutofillActionCard) - } - }, - leadingContent = { - NotificationBadge(notificationCount = 1) - }, - modifier = Modifier - .standardHorizontalMargin() - .padding(bottom = 16.dp), - ) - } - BitwardenListHeaderText( - label = stringResource(id = R.string.autofill), + BitwardenActionCard( + cardTitle = stringResource(R.string.turn_on_autofill), + actionText = stringResource(R.string.get_started), + onActionClick = autoFillHandlers.onAutofillActionCardClick, + onDismissClick = autoFillHandlers.onAutofillActionCardDismissClick, + leadingContent = { + NotificationBadge(notificationCount = 1) + }, modifier = Modifier - .fillMaxWidth() .standardHorizontalMargin() - .padding(horizontal = 16.dp), + .padding(bottom = 16.dp), ) - Spacer(modifier = Modifier.height(height = 8.dp)) + } + BitwardenListHeaderText( + label = stringResource(id = R.string.autofill), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + BitwardenSwitch( + label = stringResource(id = R.string.autofill_services), + supportingText = stringResource(id = R.string.autofill_services_explanation_long), + isChecked = state.isAutoFillServicesEnabled, + onCheckedChange = autoFillHandlers.onAutofillServicesClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .testTag("AutofillServicesSwitch") + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + if (state.showInlineAutofillOption) { BitwardenSwitch( - label = stringResource(id = R.string.autofill_services), - supportingText = stringResource(id = R.string.autofill_services_explanation_long), - isChecked = state.isAutoFillServicesEnabled, - onCheckedChange = remember(viewModel) { - { viewModel.trySendAction(AutoFillAction.AutoFillServicesClick(it)) } - }, + label = stringResource(id = R.string.inline_autofill), + supportingText = stringResource( + id = R.string.use_inline_autofill_explanation_long, + ), + isChecked = state.isUseInlineAutoFillEnabled, + onCheckedChange = autoFillHandlers.onUseInlineAutofillClick, + enabled = state.canInteractWithInlineAutofillToggle, cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .testTag("AutofillServicesSwitch") + .testTag("InlineAutofillSwitch") .standardHorizontalMargin(), ) Spacer(modifier = Modifier.height(height = 8.dp)) - if (state.showInlineAutofillOption) { - BitwardenSwitch( - label = stringResource(id = R.string.inline_autofill), - supportingText = stringResource( - id = R.string.use_inline_autofill_explanation_long, - ), - isChecked = state.isUseInlineAutoFillEnabled, - onCheckedChange = remember(viewModel) { - { viewModel.trySendAction(AutoFillAction.UseInlineAutofillClick(it)) } - }, - enabled = state.canInteractWithInlineAutofillToggle, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .testTag("InlineAutofillSwitch") - .standardHorizontalMargin(), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - - if (state.chromeAutofillSettingsOptions.isNotEmpty()) { - ChromeAutofillSettingsCard( - options = state.chromeAutofillSettingsOptions, - onOptionClicked = remember(viewModel) { - { - viewModel.trySendAction(AutoFillAction.ChromeAutofillSelected(it)) - } - }, - enabled = state.isAutoFillServicesEnabled, - ) - Spacer(modifier = Modifier.height(8.dp)) - } + } - if (state.showPasskeyManagementRow) { - BitwardenExternalLinkRow( - text = stringResource(id = R.string.passkey_management), - description = stringResource( - id = R.string.passkey_management_explanation_long, - ), - onConfirmClick = remember(viewModel) { - { viewModel.trySendAction(AutoFillAction.PasskeyManagementClick) } - }, - dialogTitle = stringResource(id = R.string.continue_to_device_settings), - dialogMessage = stringResource( - id = R.string.set_bitwarden_as_passkey_manager_description, - ), - withDivider = false, - cardStyle = CardStyle.Top(hasDivider = true), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - PrivilegedAppsRow( - text = R.string.privileged_apps.asText(), - onClick = remember(viewModel) { - { viewModel.trySendAction(AutoFillAction.TrustedAppsClick) } - }, - modifier = Modifier - .standardHorizontalMargin() - .fillMaxWidth(), - ) - Spacer(modifier = Modifier.height(8.dp)) - } - AccessibilityAutofillSwitch( - isAccessibilityAutoFillEnabled = state.isAccessibilityAutofillEnabled, - onCheckedChange = remember(viewModel) { - { viewModel.trySendAction(AutoFillAction.UseAccessibilityAutofillClick) } - }, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), - ) - Spacer(modifier = Modifier.height(16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.additional_options), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp), + if (state.chromeAutofillSettingsOptions.isNotEmpty()) { + ChromeAutofillSettingsCard( + options = state.chromeAutofillSettingsOptions, + onOptionClicked = autoFillHandlers.onChromeAutofillSelected, + enabled = state.isAutoFillServicesEnabled, ) Spacer(modifier = Modifier.height(8.dp)) - BitwardenSwitch( - label = stringResource(id = R.string.copy_totp_automatically), - supportingText = stringResource(id = R.string.copy_totp_automatically_description), - isChecked = state.isCopyTotpAutomaticallyEnabled, - onCheckedChange = remember(viewModel) { - { viewModel.trySendAction(AutoFillAction.CopyTotpAutomaticallyClick(it)) } - }, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .testTag("CopyTotpAutomaticallySwitch") - .standardHorizontalMargin(), - ) - Spacer(modifier = Modifier.height(8.dp)) - BitwardenSwitch( - label = stringResource(id = R.string.ask_to_add_login), - supportingText = stringResource(id = R.string.ask_to_add_login_description), - isChecked = state.isAskToAddLoginEnabled, - onCheckedChange = remember(viewModel) { - { viewModel.trySendAction(AutoFillAction.AskToAddLoginClick(it)) } - }, - cardStyle = CardStyle.Full, + } + + if (state.showPasskeyManagementRow) { + BitwardenExternalLinkRow( + text = stringResource(id = R.string.passkey_management), + description = stringResource( + id = R.string.passkey_management_explanation_long, + ), + onConfirmClick = autoFillHandlers.onPasskeyManagementClick, + dialogTitle = stringResource(id = R.string.continue_to_device_settings), + dialogMessage = stringResource( + id = R.string.set_bitwarden_as_passkey_manager_description, + ), + withDivider = false, + cardStyle = CardStyle.Top(hasDivider = true), modifier = Modifier .fillMaxWidth() - .testTag("AskToAddLoginSwitch") .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(8.dp)) - DefaultUriMatchTypeRow( - selectedUriMatchType = state.defaultUriMatchType, - onUriMatchTypeSelect = remember(viewModel) { - { viewModel.trySendAction(AutoFillAction.DefaultUriMatchTypeSelect(it)) } - }, + PrivilegedAppsRow( + text = R.string.privileged_apps.asText(), + onClick = autoFillHandlers.onPrivilegedAppsClick, + onHelpLinkClick = autoFillHandlers.onPrivilegedAppsHelpLinkClick, modifier = Modifier - .testTag("DefaultUriMatchDetectionChooser") .standardHorizontalMargin() .fillMaxWidth(), ) Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextRow( - text = stringResource(id = R.string.block_auto_fill), - description = stringResource( - id = R.string.auto_fill_will_not_be_offered_for_these_ur_is, - ), - onClick = remember(viewModel) { - { viewModel.trySendAction(AutoFillAction.BlockAutoFillClick) } - }, - cardStyle = CardStyle.Full, - modifier = Modifier - .standardHorizontalMargin() - .fillMaxWidth(), - ) - Spacer(modifier = Modifier.height(16.dp)) } + AccessibilityAutofillSwitch( + isAccessibilityAutoFillEnabled = state.isAccessibilityAutofillEnabled, + onCheckedChange = autoFillHandlers.onUseAccessibilityServiceClick, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.additional_options), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + BitwardenSwitch( + label = stringResource(id = R.string.copy_totp_automatically), + supportingText = stringResource(id = R.string.copy_totp_automatically_description), + isChecked = state.isCopyTotpAutomaticallyEnabled, + onCheckedChange = autoFillHandlers.onCopyTotpAutomaticallyClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .testTag("CopyTotpAutomaticallySwitch") + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(8.dp)) + BitwardenSwitch( + label = stringResource(id = R.string.ask_to_add_login), + supportingText = stringResource(id = R.string.ask_to_add_login_description), + isChecked = state.isAskToAddLoginEnabled, + onCheckedChange = autoFillHandlers.onAskToAddLoginClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .testTag("AskToAddLoginSwitch") + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(8.dp)) + DefaultUriMatchTypeRow( + selectedUriMatchType = state.defaultUriMatchType, + onUriMatchTypeSelect = autoFillHandlers.onDefaultUriMatchTypeSelect, + modifier = Modifier + .testTag("DefaultUriMatchDetectionChooser") + .standardHorizontalMargin() + .fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(8.dp)) + BitwardenTextRow( + text = stringResource(id = R.string.block_auto_fill), + description = stringResource( + id = R.string.auto_fill_will_not_be_offered_for_these_ur_is, + ), + onClick = autoFillHandlers.onBlockAutoFillClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(16.dp)) } } @@ -397,22 +395,70 @@ private fun DefaultUriMatchTypeRow( private fun PrivilegedAppsRow( text: Text, onClick: () -> Unit, + onHelpLinkClick: () -> Unit, modifier: Modifier = Modifier, ) { - BitwardenTextRow( - text = text(), - description = stringResource(R.string.privileged_apps_description), - onClick = onClick, - cardStyle = CardStyle.Bottom, - modifier = modifier, + Box( + contentAlignment = Alignment.CenterStart, + modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardStyle( + cardStyle = CardStyle.Bottom, + onClick = onClick, + clickEnabled = true, + ) + .semantics(mergeDescendants = true) { }, ) { - Icon( - painter = rememberVectorPainter(id = R.drawable.ic_chevron_right), - contentDescription = null, - tint = BitwardenTheme.colorScheme.icon.primary, - modifier = Modifier - .mirrorIfRtl() - .size(size = 16.dp), + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.fillMaxWidth(), + ) { + Text(text = text()) + Spacer(Modifier.width(8.dp)) + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_question_circle, + contentDescription = stringResource(R.string.learn_more_about_privileged_apps), + onClick = onHelpLinkClick, + contentColor = BitwardenTheme.colorScheme.icon.secondary, + modifier = Modifier.size(16.dp), + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun AutoFillContent_DynamicColorPreview() { + BitwardenTheme(dynamicColor = false) { + AutoFillContent( + state = AutoFillState( + isAskToAddLoginEnabled = true, + isAccessibilityAutofillEnabled = true, + isAutoFillServicesEnabled = true, + isCopyTotpAutomaticallyEnabled = true, + isUseInlineAutoFillEnabled = true, + showInlineAutofillOption = true, + showPasskeyManagementRow = true, + defaultUriMatchType = UriMatchType.DOMAIN, + showAutofillActionCard = true, + activeUserId = "", + chromeAutofillSettingsOptions = persistentListOf(), + ), + autoFillHandlers = AutoFillHandlers( + onAutofillActionCardClick = {}, + onAutofillActionCardDismissClick = {}, + onAutofillServicesClick = {}, + onUseInlineAutofillClick = {}, + onChromeAutofillSelected = {}, + onPasskeyManagementClick = {}, + onPrivilegedAppsClick = {}, + onUseAccessibilityServiceClick = {}, + onCopyTotpAutomaticallyClick = {}, + onAskToAddLoginClick = {}, + onDefaultUriMatchTypeSelect = {}, + onBlockAutoFillClick = {}, + onPrivilegedAppsHelpLinkClick = {}, + ), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt index 42999063396..8fba190cb8b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt @@ -113,6 +113,7 @@ class AutoFillViewModel @Inject constructor( AutoFillAction.DismissShowAutofillActionCard -> handleDismissShowAutofillActionCard() is AutoFillAction.ChromeAutofillSelected -> handleChromeAutofillSelected(action) AutoFillAction.TrustedAppsClick -> handleTrustedAppsClick() + AutoFillAction.TrustedAppsHelpLinkClick -> handleTrustedAppsHelpLinkClick() } private fun handleInternalAction(action: AutoFillAction.Internal) { @@ -155,6 +156,10 @@ class AutoFillViewModel @Inject constructor( sendEvent(AutoFillEvent.NavigateToPrivilegedApps) } + private fun handleTrustedAppsHelpLinkClick() { + sendEvent(AutoFillEvent.NavigateToAboutPrivilegedApps) + } + private fun handleDismissShowAutofillActionCard() { dismissShowAutofillActionCard() } @@ -330,9 +335,14 @@ sealed class AutoFillEvent { data object NavigateToSetupAutofill : AutoFillEvent() /** - * Navigates to the privileged apps screen. + * Navigate to the privileged apps screen. + */ + data object NavigateToPrivilegedApps : AutoFillEvent() + + /** + * Navigate to the about privileged apps screen. */ - object NavigateToPrivilegedApps : AutoFillEvent() + data object NavigateToAboutPrivilegedApps : AutoFillEvent() } /** @@ -407,7 +417,12 @@ sealed class AutoFillAction { /** * User has clicked the trusted apps action card. */ - object TrustedAppsClick : AutoFillAction() + data object TrustedAppsClick : AutoFillAction() + + /** + * User has clicked the trusted apps help link. + */ + data object TrustedAppsHelpLinkClick : AutoFillAction() /** * User has clicked one of the chrome autofill options. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/handlers/AutoFillHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/handlers/AutoFillHandlers.kt new file mode 100644 index 00000000000..0513c795879 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/handlers/AutoFillHandlers.kt @@ -0,0 +1,93 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.handlers + +import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeReleaseChannel +import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.AutoFillAction +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.AutoFillViewModel + +/** + * Handlers for the AutoFill screen. + */ +@Suppress("LongParameterList") +class AutoFillHandlers( + val onAutofillActionCardClick: () -> Unit, + val onAutofillActionCardDismissClick: () -> Unit, + val onAutofillServicesClick: (isEnabled: Boolean) -> Unit, + val onUseInlineAutofillClick: (isEnabled: Boolean) -> Unit, + val onChromeAutofillSelected: (releaseChannel: ChromeReleaseChannel) -> Unit, + val onPasskeyManagementClick: () -> Unit, + val onPrivilegedAppsClick: () -> Unit, + val onPrivilegedAppsHelpLinkClick: () -> Unit, + val onUseAccessibilityServiceClick: () -> Unit, + val onCopyTotpAutomaticallyClick: (isEnabled: Boolean) -> Unit, + val onAskToAddLoginClick: (isEnabled: Boolean) -> Unit, + val onDefaultUriMatchTypeSelect: (defaultUriMatchType: UriMatchType) -> Unit, + val onBlockAutoFillClick: () -> Unit, +) { + @Suppress("UndocumentedPublicClass") + companion object { + + /** + * Creates a new instance of [AutoFillHandlers] from the given [AutoFillViewModel]. + */ + fun create(viewModel: AutoFillViewModel): AutoFillHandlers = AutoFillHandlers( + onAutofillActionCardClick = { + viewModel.trySendAction(AutoFillAction.AutofillActionCardCtaClick) + }, + onAutofillActionCardDismissClick = { + viewModel.trySendAction(AutoFillAction.DismissShowAutofillActionCard) + }, + onAutofillServicesClick = { + viewModel.trySendAction( + AutoFillAction.AutoFillServicesClick( + it, + ), + ) + }, + onUseInlineAutofillClick = { + viewModel.trySendAction( + AutoFillAction.UseInlineAutofillClick( + it, + ), + ) + }, + onChromeAutofillSelected = { + viewModel.trySendAction( + AutoFillAction.ChromeAutofillSelected( + it, + ), + ) + }, + onPasskeyManagementClick = { + viewModel.trySendAction(AutoFillAction.PasskeyManagementClick) + }, + onPrivilegedAppsClick = { viewModel.trySendAction(AutoFillAction.TrustedAppsClick) }, + onPrivilegedAppsHelpLinkClick = { + viewModel.trySendAction(AutoFillAction.TrustedAppsHelpLinkClick) + }, + onUseAccessibilityServiceClick = { + viewModel.trySendAction( + AutoFillAction.UseAccessibilityAutofillClick, + ) + }, + onCopyTotpAutomaticallyClick = { + viewModel.trySendAction( + AutoFillAction.CopyTotpAutomaticallyClick( + it, + ), + ) + }, + onAskToAddLoginClick = { + viewModel.trySendAction(AutoFillAction.AskToAddLoginClick(it)) + }, + onDefaultUriMatchTypeSelect = { + viewModel.trySendAction( + AutoFillAction.DefaultUriMatchTypeSelect( + it, + ), + ) + }, + onBlockAutoFillClick = { viewModel.trySendAction(AutoFillAction.BlockAutoFillClick) }, + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/about/AboutPrivilegedAppsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/about/AboutPrivilegedAppsNavigation.kt new file mode 100644 index 00000000000..8d63d9f872c --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/about/AboutPrivilegedAppsNavigation.kt @@ -0,0 +1,32 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedapps.about + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions + +private const val ABOUT_PRIVILEGED_APPS_ROUTE = "about_privileged_apps" + +/** + * Add about privileged apps destination to the nav graph. + */ +fun NavGraphBuilder.aboutPrivilegedAppsDestination( + navigateBack: () -> Unit, +) { + composableWithPushTransitions( + route = ABOUT_PRIVILEGED_APPS_ROUTE, + ) { + AboutPrivilegedAppsScreen( + onNavigateBack = navigateBack, + ) + } +} + +/** + * Navigate to the about privileged apps screen. + */ +fun NavController.navigateToAboutPrivilegedAppsScreen( + navOptions: NavOptions? = null, +) { + navigate(ABOUT_PRIVILEGED_APPS_ROUTE, navOptions) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/about/AboutPrivilegedAppsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/about/AboutPrivilegedAppsScreen.kt new file mode 100644 index 00000000000..d6aa0842ffc --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/about/AboutPrivilegedAppsScreen.kt @@ -0,0 +1,98 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedapps.about + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle +import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow +import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold +import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter +import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme + +/** + * Top level composable for the About Privileged Apps screen. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AboutPrivilegedAppsScreen( + onNavigateBack: () -> Unit, +) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + BitwardenScaffold( + topBar = { + BitwardenTopAppBar( + title = stringResource(R.string.about_privileged_applications), + scrollBehavior = scrollBehavior, + navigationIcon = rememberVectorPainter(id = R.drawable.ic_back), + navigationIconContentDescription = stringResource(id = R.string.back), + onNavigationIconClick = remember { onNavigateBack }, + ) + }, + ) { + AboutPrivilegedAppsContent() + } +} + +@Composable +private fun AboutPrivilegedAppsContent() { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) { + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(R.string.privileged_apps_description), + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(16.dp)) + Column { + BitwardenTextRow( + text = stringResource(R.string.trusted_by_you), + cardStyle = CardStyle.Top(), + onClick = {}, + description = stringResource(R.string.trusted_by_you_learn_more), + ) + BitwardenTextRow( + text = stringResource(R.string.trusted_by_the_community), + cardStyle = CardStyle.Middle(), + onClick = {}, + description = stringResource(R.string.trusted_by_community_learn_more), + ) + BitwardenTextRow( + text = stringResource(R.string.trusted_by_google), + cardStyle = CardStyle.Bottom, + onClick = {}, + description = stringResource(R.string.trusted_by_google_learn_more), + ) + } + Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) + } +} + +@Preview(showBackground = true) +@Composable +private fun AboutPrivilegedAppsContent_Preview() { + BitwardenTheme { + AboutPrivilegedAppsContent() + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsListNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/PrivilegedAppsListNavigation.kt similarity index 97% rename from app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsListNavigation.kt rename to app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/PrivilegedAppsListNavigation.kt index 646a19d2c0d..1c9fd46c47f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsListNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/PrivilegedAppsListNavigation.kt @@ -1,4 +1,4 @@ -package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedapps.list import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsListScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/PrivilegedAppsListScreen.kt similarity index 56% rename from app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsListScreen.kt rename to app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/PrivilegedAppsListScreen.kt index 838cd022842..9b28348f38a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsListScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/PrivilegedAppsListScreen.kt @@ -1,43 +1,28 @@ -package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedapps.list -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect -import com.x8bit.bitwarden.ui.platform.base.util.cardStyle import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar @@ -46,13 +31,10 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText -import com.x8bit.bitwarden.ui.platform.components.model.CardStyle +import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter -import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager -import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist.model.PrivilegedAppListItem -import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager -import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedapps.list.model.PrivilegedAppListItem import kotlinx.collections.immutable.toImmutableList /** @@ -63,7 +45,6 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun PrivilegedAppsListScreen( onNavigateBack: () -> Unit, - intentManager: IntentManager = LocalIntentManager.current, viewModel: PrivilegedAppsViewModel = hiltViewModel(), ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() @@ -72,9 +53,6 @@ fun PrivilegedAppsListScreen( EventsEffect(viewModel) { event -> when (event) { PrivilegedAppsListEvent.NavigateBack -> onNavigateBack() - is PrivilegedAppsListEvent.NavigateToUri -> { - intentManager.launchUri(uri = event.uri) - } } } @@ -83,27 +61,6 @@ fun PrivilegedAppsListScreen( BitwardenLoadingDialog(stringResource(R.string.loading)) } - is Fido2TrustState.DialogState.ConfirmLaunchUri -> { - BitwardenTwoButtonDialog( - title = dialogState.title.invoke(), - message = dialogState.message.invoke(), - confirmButtonText = stringResource(R.string.continue_text), - dismissButtonText = stringResource(R.string.cancel), - onConfirmClick = remember { - { - viewModel.trySendAction(PrivilegedAppsListAction.DismissDialogClick) - intentManager.launchUri(dialogState.uri) - } - }, - onDismissClick = remember { - { viewModel.trySendAction(PrivilegedAppsListAction.DismissDialogClick) } - }, - onDismissRequest = remember { - { viewModel.trySendAction(PrivilegedAppsListAction.DismissDialogClick) } - }, - ) - } - is Fido2TrustState.DialogState.ConfirmDeleteTrustedApp -> { BitwardenTwoButtonDialog( title = stringResource(R.string.delete), @@ -154,19 +111,6 @@ fun PrivilegedAppsListScreen( onNavigationIconClick = remember(viewModel) { { viewModel.trySendAction(PrivilegedAppsListAction.BackClick) } }, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_external_link, - contentDescription = stringResource(R.string.bitwarden_help_center), - onClick = remember(viewModel) { - { - viewModel.trySendAction( - action = PrivilegedAppsListAction.LaunchHelpCenterClick, - ) - } - }, - ) - }, ) }, modifier = Modifier @@ -196,58 +140,78 @@ private fun PrivilegedAppsListContent( if (state.userTrustedApps.isNotEmpty()) { item(key = "trusted_by_you") { Spacer(modifier = Modifier.height(12.dp)) - PrivilegedAppHeaderItem( - headerText = stringResource(R.string.trusted_by_you), - learnMoreText = stringResource(R.string.trusted_by_you_learn_more), - itemCount = state.userTrustedApps.size, - modifier = Modifier.fillMaxWidth(), + BitwardenListHeaderText( + label = stringResource(R.string.trusted_by_you), + supportingLabel = state.userTrustedApps.size.toString(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp) + .animateItem(), ) Spacer(modifier = Modifier.height(8.dp)) } + itemsIndexed( key = { _, item -> "userTrust_${item.packageName}_${item.signature}" }, items = state.userTrustedApps, ) { index, item -> - PrivilegedAppListItem( - item = item, - canDelete = true, - onClick = remember(item) { - { onDeleteClick(item) } - }, + BitwardenTextRow( + text = item.packageName, + onClick = {}, cardStyle = state.userTrustedApps - .toListItemCardStyle(index = index), + .toListItemCardStyle(index), + description = item.signature, modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) + .padding(start = 16.dp) .animateItem(), - ) + ) { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_send_pending_delete, + contentDescription = "", + onClick = remember(item) { + { onDeleteClick(item) } + }, + ) + } } } if (state.communityTrustedApps.isNotEmpty()) { item(key = "trusted_by_community") { - Spacer(modifier = Modifier.height(12.dp)) - PrivilegedAppHeaderItem( - headerText = stringResource(R.string.trusted_by_the_community), - learnMoreText = stringResource(R.string.trusted_by_community_learn_more), - itemCount = state.communityTrustedApps.size, - modifier = Modifier.fillMaxWidth(), + Spacer( + modifier = Modifier + .height(if (state.userTrustedApps.isEmpty()) 12.dp else 16.dp) + .animateItem(), + ) + BitwardenListHeaderText( + label = stringResource(R.string.trusted_by_the_community), + supportingLabel = state.communityTrustedApps.size.toString(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp) + .animateItem(), + ) + Spacer( + modifier = Modifier + .height(8.dp) + .animateItem(), ) - Spacer(modifier = Modifier.height(8.dp)) } + itemsIndexed( key = { _, item -> "communityTrust_${item.packageName}_${item.signature}" }, items = state.communityTrustedApps, ) { index, item -> - PrivilegedAppListItem( - item = item, - canDelete = false, + BitwardenTextRow( + text = item.packageName, onClick = {}, cardStyle = state.communityTrustedApps - .toListItemCardStyle(index = index), + .toListItemCardStyle(index), + description = item.signature, modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() + .padding(start = 16.dp) .animateItem(), ) } @@ -255,135 +219,51 @@ private fun PrivilegedAppsListContent( if (state.googleTrustedApps.isNotEmpty()) { item(key = "trusted_by_google") { - Spacer(modifier = Modifier.height(12.dp)) - PrivilegedAppHeaderItem( - headerText = stringResource(R.string.trusted_by_google), - learnMoreText = stringResource(R.string.trusted_by_google_learn_more), - itemCount = state.googleTrustedApps.size, - modifier = Modifier.fillMaxWidth(), + Spacer(modifier = Modifier.height(16.dp)) + BitwardenListHeaderText( + label = stringResource(R.string.trusted_by_google), + supportingLabel = state.googleTrustedApps.size.toString(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp) + .animateItem(), + ) + Spacer( + modifier = Modifier + .height(8.dp) + .animateItem(), ) - Spacer(modifier = Modifier.height(8.dp)) } + itemsIndexed( key = { _, item -> "googleTrust_${item.packageName}_${item.signature}" }, items = state.googleTrustedApps, ) { index, item -> - PrivilegedAppListItem( - item = item, - canDelete = false, - onClick = { }, + BitwardenTextRow( + text = item.packageName, + onClick = {}, cardStyle = state.googleTrustedApps .toListItemCardStyle(index), + description = item.signature, modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() + .padding(start = 16.dp) .animateItem(), ) } } item { - Spacer(modifier = Modifier.height(88.dp)) + Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } } +// region Previews +@Preview(showBackground = true) @Composable -private fun LazyItemScope.PrivilegedAppHeaderItem( - headerText: String, - learnMoreText: String, - itemCount: Int, - modifier: Modifier = Modifier, -) { - var showDialog by rememberSaveable { mutableStateOf(false) } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .standardHorizontalMargin(), - ) { - BitwardenListHeaderText( - label = headerText, - supportingLabel = itemCount.toString(), - modifier = Modifier.animateItem(), - ) - val size by animateDpAsState( - targetValue = 16.dp, - label = "${headerText}_animation", - ) - Spacer(modifier = Modifier.width(width = 8.dp)) - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_question_circle_small, - contentDescription = "", - onClick = { showDialog = !showDialog }, - contentColor = BitwardenTheme.colorScheme.icon.secondary, - modifier = Modifier.size(size), - ) - } - - if (showDialog) { - BitwardenBasicDialog( - title = headerText, - message = learnMoreText, - onDismissRequest = { showDialog = false }, - ) - } -} - -@Composable -private fun LazyItemScope.PrivilegedAppListItem( - item: PrivilegedAppListItem, - canDelete: Boolean, - onClick: () -> Unit, - cardStyle: CardStyle, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .defaultMinSize(minHeight = 60.dp) - .cardStyle( - cardStyle = cardStyle, - paddingStart = 16.dp, - paddingEnd = if (canDelete) 4.dp else 16.dp, - ), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - Text( - modifier = Modifier, - text = item.packageName, - style = BitwardenTheme.typography.bodyLarge, - color = BitwardenTheme.colorScheme.text.primary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = item.signature, - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - if (canDelete) { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_send_pending_delete, - contentDescription = "", - onClick = onClick, - ) - } - } -} - -@Preview -@Composable -private fun PrivilegedAppsListScreenPreview() { +private fun PrivilegedAppsListScreen_Preview() { PrivilegedAppsListContent( state = Fido2TrustState( googleTrustedApps = listOf( @@ -436,21 +316,4 @@ private fun PrivilegedAppsListScreenPreview() { onDeleteClick = {}, ) } - -@Preview(showBackground = true) -@Composable -private fun PrivilegedAppListItemPreview() { - LazyColumn { - item { - PrivilegedAppListItem( - item = PrivilegedAppListItem( - packageName = "com.google.android.apps.walletnfcrel", - signature = "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX", - ), - canDelete = false, - onClick = {}, - cardStyle = CardStyle.Middle(hasDivider = false), - ) - } - } -} +//endregion Previews diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/PrivilegedAppsListViewModel.kt similarity index 80% rename from app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsViewModel.kt rename to app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/PrivilegedAppsListViewModel.kt index 16a0bcdaf36..f6cd19137a0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/PrivilegedAppsViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/PrivilegedAppsListViewModel.kt @@ -1,8 +1,6 @@ -package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedapps.list -import android.net.Uri import android.os.Parcelable -import androidx.core.net.toUri import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.x8bit.bitwarden.R @@ -13,9 +11,10 @@ import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.asText -import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist.model.PrivilegedAppListItem +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedapps.list.model.PrivilegedAppListItem import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -26,12 +25,11 @@ import kotlinx.parcelize.Parcelize import javax.inject.Inject private const val KEY_STATE = "state" -private val BITWARDEN_HELP_CENTER_USING_PASSKEYS_URI = - "https://bitwarden.com/help/storing-passkeys/#using-passkeys-with-bitwarden".toUri() /** * View model for the [PrivilegedAppsListScreen]. */ +@Suppress("TooManyFunctions") @HiltViewModel class PrivilegedAppsViewModel @Inject constructor( private val privilegedAppRepository: PrivilegedAppRepository, @@ -39,12 +37,9 @@ class PrivilegedAppsViewModel @Inject constructor( ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: Fido2TrustState( - googleTrustedApps = emptyList() - .toImmutableList(), - communityTrustedApps = emptyList() - .toImmutableList(), - userTrustedApps = emptyList() - .toImmutableList(), + googleTrustedApps = persistentListOf(), + communityTrustedApps = persistentListOf(), + userTrustedApps = persistentListOf(), dialogState = null, ), ) { @@ -60,7 +55,7 @@ class PrivilegedAppsViewModel @Inject constructor( override fun handleAction(action: PrivilegedAppsListAction) { when (action) { is PrivilegedAppsListAction.UserTrustedAppDeleteClick -> { - handleUserTrustedAppDeleteClick(action.app) + handleUserTrustedAppDeleteClick(action) } is PrivilegedAppsListAction.UserTrustedAppDeleteConfirmClick -> { @@ -68,30 +63,27 @@ class PrivilegedAppsViewModel @Inject constructor( } is PrivilegedAppsListAction.DismissDialogClick -> { - mutableStateFlow.update { it.copy(dialogState = null) } + handleDismissDialogClick() } - is PrivilegedAppsListAction.LaunchHelpCenterClick -> { - mutableStateFlow.update { - it.copy( - dialogState = Fido2TrustState.DialogState.ConfirmLaunchUri( - title = R.string.continue_to_help_center.asText(), - message = R.string.learn_more_about_using_passkeys_with_bitwarden - .asText(), - uri = BITWARDEN_HELP_CENTER_USING_PASSKEYS_URI, - ), - ) - } + is PrivilegedAppsListAction.BackClick -> { + handleBackClick() } - is PrivilegedAppsListAction.BackClick -> sendEvent(PrivilegedAppsListEvent.NavigateBack) - is PrivilegedAppsListAction.Internal.PrivilegedAppDataStateReceive -> { handleTrustedAppDataStateReceive(action.dataState) } } } + private fun handleBackClick() { + sendEvent(PrivilegedAppsListEvent.NavigateBack) + } + + private fun handleDismissDialogClick() { + mutableStateFlow.update { it.copy(dialogState = null) } + } + private fun handleTrustedAppDataStateReceive(dataState: DataState) { when (dataState) { is DataState.Loaded -> handleTrustedAppDataStateLoaded(dataState) @@ -155,10 +147,12 @@ class PrivilegedAppsViewModel @Inject constructor( } } - private fun handleUserTrustedAppDeleteClick(app: PrivilegedAppListItem) { + private fun handleUserTrustedAppDeleteClick( + action: PrivilegedAppsListAction.UserTrustedAppDeleteClick, + ) { mutableStateFlow.update { it.copy( - dialogState = Fido2TrustState.DialogState.ConfirmDeleteTrustedApp(app), + dialogState = Fido2TrustState.DialogState.ConfirmDeleteTrustedApp(action.app), ) } } @@ -228,16 +222,6 @@ data class Fido2TrustState( data class General( val message: Text, ) : DialogState() - - /** - * Show the confirm launch URI dialog. - */ - @Parcelize - data class ConfirmLaunchUri( - val title: Text, - val message: Text, - val uri: Uri, - ) : DialogState() } } @@ -250,11 +234,6 @@ sealed class PrivilegedAppsListEvent { * Navigate back to the previous screen. */ data object NavigateBack : PrivilegedAppsListEvent() - - /** - * Navigate to the given [uri]. - */ - data class NavigateToUri(val uri: Uri) : PrivilegedAppsListEvent() } /** @@ -271,11 +250,6 @@ sealed class PrivilegedAppsListAction { */ data object DismissDialogClick : PrivilegedAppsListAction() - /** - * The user has clicked the help center button. - */ - data object LaunchHelpCenterClick : PrivilegedAppsListAction() - /** * The user has selected to delete a trusted app from their local trust store. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/model/PrivilegedAppListItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/model/PrivilegedAppListItem.kt similarity index 94% rename from app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/model/PrivilegedAppListItem.kt rename to app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/model/PrivilegedAppListItem.kt index d888c329770..f7438c7a7a6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedappslist/model/PrivilegedAppListItem.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/model/PrivilegedAppListItem.kt @@ -1,4 +1,4 @@ -package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist.model +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedapps.list.model import android.os.Parcelable import kotlinx.parcelize.Parcelize diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e760c22c94d..4909c4af1c0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1233,13 +1233,18 @@ Do you want to switch to this account? Unknown Application not trusted Privileged apps - Trusted by Google - Trusted by you - Trusted by the community + Trusted by Google (%d) + Trusted by you (%d) + Trusted by the community (%d) Are you sure you want to stop trusting %s? Learn more about using passkeys with Bitwarden. - These are applications YOU trust to perform passkey operations on behalf of other parties. These are often web browsers, and are not currently trusted by Google or the Bitwarden community. - These are applications the Bitwarden Community Members trust to perform passkey operations on behalf of other parties. These are often web browsers, and are not distributed through Google\'s Play Store. They are used and considered safe by our community members. - These are applications Google considers safe to perform passkey operations on behalf of other parties. These are often web browsers, and are available in Google\'s Play Store.\n - Applications, often web browsers, trusted to perform passkey operations on behalf of other parties. + These are applications or browsers that Bitwarden does not trust by default, but YOU trust to perform passkey operations. + These are applications not included in the Google Play Store, but Bitwarden trusts to perform passkey operations after community members use and report them as safe. + These are applications Google considers safe and are available in Google\'s Play Store. + To protect users from phishing attempts, by default, Bitwarden only completes passkey operations through web browsers trusted by Google or the Bitwarden community. + About privileged applications + Trusted by You + Trusted by the Community + Trusted by Google + Learn more about privileged apps From b4fd2dadb869b551a450e87abbe9ba72474dc336 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Wed, 19 Mar 2025 09:06:48 -0400 Subject: [PATCH 3/5] Add delete vector icon and update delete button icon in privileged apps list - Added a new vector graphic file `ic_delete.xml` for the delete icon. - Updated the delete button icon in the `PrivilegedAppsListScreen` from `ic_send_pending_delete` to `ic_delete`. --- .../privilegedapps/list/PrivilegedAppsListScreen.kt | 2 +- app/src/main/res/drawable/ic_delete.xml | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable/ic_delete.xml diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/PrivilegedAppsListScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/PrivilegedAppsListScreen.kt index 9b28348f38a..dcd1d9f6fda 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/PrivilegedAppsListScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/privilegedapps/list/PrivilegedAppsListScreen.kt @@ -167,7 +167,7 @@ private fun PrivilegedAppsListContent( .animateItem(), ) { BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_send_pending_delete, + vectorIconRes = R.drawable.ic_delete, contentDescription = "", onClick = remember(item) { { onDeleteClick(item) } diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 00000000000..fa5b7166e3f --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,10 @@ + + + From bdce8050b6bb153e70e63da5e7cb05586f2185ca Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Wed, 19 Mar 2025 15:25:31 -0400 Subject: [PATCH 4/5] Remove unused strings - Removed unused string resources: `trusted_by_google_x`, `trusted_by_you_x`, and `trusted_by_the_community_c`. --- app/src/main/res/values/strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4909c4af1c0..2c30f7c1a39 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1233,9 +1233,6 @@ Do you want to switch to this account? Unknown Application not trusted Privileged apps - Trusted by Google (%d) - Trusted by you (%d) - Trusted by the community (%d) Are you sure you want to stop trusting %s? Learn more about using passkeys with Bitwarden. These are applications or browsers that Bitwarden does not trust by default, but YOU trust to perform passkey operations. From 3c1ef13c79bda5db505b32375cd89bc7bc26c3fb Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Wed, 19 Mar 2025 15:45:45 -0400 Subject: [PATCH 5/5] Fix compilation failure --- .../platform/feature/settings/autofill/AutoFillScreenTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt index 0ab401de94f..bd1eb72a2db 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt @@ -40,6 +40,7 @@ class AutoFillScreenTest : BaseComposeTest() { private var onNavigateToBlockAutoFillScreenCalled = false private var onNavigateToSetupAutoFillScreenCalled = false private var onNavigateToPrivilegedAppsScreenCalled = false + private var onNavigateToAboutPrivilegedAppsScreenCalled = false private val mutableEventFlow = bufferedMutableSharedFlow() private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) @@ -66,6 +67,9 @@ class AutoFillScreenTest : BaseComposeTest() { onNavigateToPrivilegedAppsScreen = { onNavigateToPrivilegedAppsScreenCalled = true }, + onNavigateToAboutPrivilegedAppsScreen = { + onNavigateToAboutPrivilegedAppsScreenCalled = true + }, viewModel = viewModel, ) }