mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 00:06:22 -06:00
[PM-23723] URI Matching detection layout updates on advanced options (#5574)
This commit is contained in:
parent
3ed63ef5eb
commit
a688693f43
@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -26,9 +27,13 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.core.util.persistentListOfNotNull
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.annotatedStringResource
|
||||
import com.bitwarden.ui.platform.base.util.spanStyleOf
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.badge.NotificationBadge
|
||||
@ -40,10 +45,12 @@ import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.dropdown.model.MultiSelectOption
|
||||
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
|
||||
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow
|
||||
@ -52,6 +59,7 @@ import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.BrowserAutofillSettingsCard
|
||||
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.feature.settings.autofill.util.isAdvancedMatching
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@ -109,6 +117,10 @@ fun AutoFillScreen(
|
||||
AutoFillEvent.NavigateToPrivilegedAppsListScreen -> {
|
||||
onNavigateToPrivilegedAppsList()
|
||||
}
|
||||
|
||||
AutoFillEvent.NavigateToLearnMore -> {
|
||||
intentManager.launchUri("https://bitwarden.com/help/uri-match-detection/".toUri())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -303,6 +315,7 @@ private fun AutoFillScreenContent(
|
||||
DefaultUriMatchTypeRow(
|
||||
selectedUriMatchType = state.defaultUriMatchType,
|
||||
onUriMatchTypeSelect = autoFillHandlers.onDefaultUriMatchTypeSelect,
|
||||
onNavigateToLearnMore = autoFillHandlers.onLearnMoreClick,
|
||||
modifier = Modifier
|
||||
.testTag("DefaultUriMatchDetectionChooser")
|
||||
.standardHorizontalMargin()
|
||||
@ -387,24 +400,205 @@ private fun AccessibilityAutofillSwitch(
|
||||
private fun DefaultUriMatchTypeRow(
|
||||
selectedUriMatchType: UriMatchType,
|
||||
onUriMatchTypeSelect: (UriMatchType) -> Unit,
|
||||
onNavigateToLearnMore: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showAdvancedDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var optionPendingConfirmation by rememberSaveable { mutableStateOf<UriMatchType?>(null) }
|
||||
var shouldShowLearnMoreMatchDetectionDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
UriMatchSelectionButton(
|
||||
selectedUriMatchType = selectedUriMatchType,
|
||||
onOptionSelected = { selectedOption ->
|
||||
if (selectedOption.isAdvancedMatching()) {
|
||||
optionPendingConfirmation = selectedOption
|
||||
showAdvancedDialog = true
|
||||
} else {
|
||||
onUriMatchTypeSelect(selectedOption)
|
||||
optionPendingConfirmation = null
|
||||
showAdvancedDialog = false
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
val currentOptionToConfirm = optionPendingConfirmation
|
||||
if (showAdvancedDialog && currentOptionToConfirm != null) {
|
||||
AdvancedMatchDetectionWarningDialog(
|
||||
pendingOption = currentOptionToConfirm,
|
||||
onDialogConfirm = {
|
||||
onUriMatchTypeSelect(currentOptionToConfirm)
|
||||
showAdvancedDialog = false
|
||||
optionPendingConfirmation = null
|
||||
shouldShowLearnMoreMatchDetectionDialog = true
|
||||
},
|
||||
onDialogDismiss = {
|
||||
showAdvancedDialog = false
|
||||
optionPendingConfirmation = null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (shouldShowLearnMoreMatchDetectionDialog) {
|
||||
MatchDetectionLearnMoreDialog(
|
||||
uriMatchType = selectedUriMatchType,
|
||||
onDialogConfirm = {
|
||||
onNavigateToLearnMore()
|
||||
shouldShowLearnMoreMatchDetectionDialog = false
|
||||
},
|
||||
onDialogDismiss = {
|
||||
shouldShowLearnMoreMatchDetectionDialog = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AdvancedMatchDetectionWarningDialog(
|
||||
pendingOption: UriMatchType,
|
||||
onDialogConfirm: () -> Unit,
|
||||
onDialogDismiss: () -> Unit,
|
||||
) {
|
||||
val descriptionStringResId =
|
||||
when (pendingOption) {
|
||||
UriMatchType.STARTS_WITH -> {
|
||||
BitwardenString.advanced_option_with_increased_risk_of_exposing_credentials
|
||||
}
|
||||
|
||||
UriMatchType.REGULAR_EXPRESSION -> {
|
||||
BitwardenString.advanced_option_increased_risk_exposing_credentials_used_incorrectly
|
||||
}
|
||||
|
||||
UriMatchType.HOST,
|
||||
UriMatchType.DOMAIN,
|
||||
UriMatchType.EXACT,
|
||||
UriMatchType.NEVER,
|
||||
-> {
|
||||
error("Unexpected value $pendingOption on AdvancedMatchDetectionWarningDialog")
|
||||
}
|
||||
}
|
||||
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(
|
||||
id = BitwardenString.are_you_sure_you_want_to_use,
|
||||
formatArgs = arrayOf(
|
||||
pendingOption.displayLabel(),
|
||||
),
|
||||
),
|
||||
message = stringResource(
|
||||
id = descriptionStringResId,
|
||||
),
|
||||
confirmButtonText = stringResource(id = BitwardenString.yes),
|
||||
dismissButtonText = stringResource(id = BitwardenString.cancel),
|
||||
onConfirmClick = onDialogConfirm,
|
||||
onDismissClick = onDialogDismiss,
|
||||
onDismissRequest = onDialogDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UriMatchSelectionButton(
|
||||
selectedUriMatchType: UriMatchType,
|
||||
onOptionSelected: (UriMatchType) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
resources: Resources = LocalContext.current.resources,
|
||||
) {
|
||||
val advancedOptions = UriMatchType.entries.filter { it.isAdvancedMatching() }
|
||||
val options = persistentListOfNotNull(
|
||||
*UriMatchType
|
||||
.entries
|
||||
.filter { !it.isAdvancedMatching() }
|
||||
.map { MultiSelectOption.Row(it.displayLabel()) }
|
||||
.toTypedArray(),
|
||||
if (advancedOptions.isNotEmpty()) {
|
||||
MultiSelectOption.Header(
|
||||
title = stringResource(id = BitwardenString.advanced_options),
|
||||
testTag = "AdvancedOptionsSection",
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
*advancedOptions
|
||||
.map { MultiSelectOption.Row(it.displayLabel()) }
|
||||
.toTypedArray(),
|
||||
)
|
||||
|
||||
BitwardenMultiSelectButton(
|
||||
label = stringResource(id = BitwardenString.default_uri_match_detection),
|
||||
options = UriMatchType.entries.map { it.displayLabel() }.toImmutableList(),
|
||||
selectedOption = selectedUriMatchType.displayLabel(),
|
||||
onOptionSelected = { selectedOption ->
|
||||
onUriMatchTypeSelect(
|
||||
UriMatchType
|
||||
.entries
|
||||
.first { it.displayLabel.toString(resources) == selectedOption },
|
||||
)
|
||||
options = options,
|
||||
selectedOption = MultiSelectOption.Row(selectedUriMatchType.displayLabel()),
|
||||
onOptionSelected = { row ->
|
||||
val newSelectedType = UriMatchType
|
||||
.entries
|
||||
.first { it.displayLabel(resources) == row.title }
|
||||
onOptionSelected(newSelectedType)
|
||||
},
|
||||
supportingText = stringResource(
|
||||
id = BitwardenString.default_uri_match_detection_description,
|
||||
),
|
||||
cardStyle = CardStyle.Full,
|
||||
supportingContent = { SupportingTextForMatchDetection(selectedUriMatchType) },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MatchDetectionLearnMoreDialog(
|
||||
uriMatchType: UriMatchType,
|
||||
onDialogConfirm: () -> Unit,
|
||||
onDialogDismiss: () -> Unit,
|
||||
) {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = BitwardenString.keep_your_credential_secure),
|
||||
message = stringResource(
|
||||
id = BitwardenString.learn_more_about_how_to_keep_credentirals_secure,
|
||||
formatArgs = arrayOf(uriMatchType.displayLabel()),
|
||||
),
|
||||
confirmButtonText = stringResource(id = BitwardenString.learn_more),
|
||||
dismissButtonText = stringResource(id = BitwardenString.close),
|
||||
onConfirmClick = onDialogConfirm,
|
||||
onDismissClick = onDialogDismiss,
|
||||
onDismissRequest = onDialogDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SupportingTextForMatchDetection(
|
||||
uriMatchType: UriMatchType,
|
||||
) {
|
||||
val stringResId =
|
||||
when (uriMatchType) {
|
||||
UriMatchType.STARTS_WITH -> {
|
||||
BitwardenString.default_uri_match_detection_description_advanced_options
|
||||
}
|
||||
|
||||
UriMatchType.REGULAR_EXPRESSION -> {
|
||||
BitwardenString.default_uri_match_detection_description_advanced_options_incorrectly
|
||||
}
|
||||
|
||||
UriMatchType.HOST,
|
||||
UriMatchType.DOMAIN,
|
||||
UriMatchType.EXACT,
|
||||
UriMatchType.NEVER,
|
||||
-> {
|
||||
BitwardenString.default_uri_match_detection_description
|
||||
}
|
||||
}
|
||||
|
||||
val supportingAnnotatedString =
|
||||
annotatedStringResource(
|
||||
id = stringResId,
|
||||
emphasisHighlightStyle = spanStyleOf(
|
||||
textStyle = BitwardenTheme.typography.bodyMediumEmphasis,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
),
|
||||
style = spanStyleOf(
|
||||
textStyle = BitwardenTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = supportingAnnotatedString,
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -131,12 +131,17 @@ class AutoFillViewModel @Inject constructor(
|
||||
is AutoFillAction.BrowserAutofillSelected -> handleBrowserAutofillSelected(action)
|
||||
AutoFillAction.AboutPrivilegedAppsClick -> handleAboutPrivilegedAppsClick()
|
||||
AutoFillAction.PrivilegedAppsClick -> handlePrivilegedAppsClick()
|
||||
AutoFillAction.LearnMoreClick -> handleLearnMoreClick()
|
||||
}
|
||||
|
||||
private fun handlePrivilegedAppsClick() {
|
||||
sendEvent(AutoFillEvent.NavigateToPrivilegedAppsListScreen)
|
||||
}
|
||||
|
||||
private fun handleLearnMoreClick() {
|
||||
sendEvent(AutoFillEvent.NavigateToLearnMore)
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: AutoFillAction.Internal) {
|
||||
when (action) {
|
||||
is AutoFillAction.Internal.AccessibilityEnabledUpdateReceive -> {
|
||||
@ -390,6 +395,11 @@ sealed class AutoFillEvent {
|
||||
* Navigate to the privileged apps list screen.
|
||||
*/
|
||||
data object NavigateToPrivilegedAppsListScreen : AutoFillEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the learn more.
|
||||
*/
|
||||
data object NavigateToLearnMore : AutoFillEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -476,6 +486,11 @@ sealed class AutoFillAction {
|
||||
*/
|
||||
data object PrivilegedAppsClick : AutoFillAction()
|
||||
|
||||
/**
|
||||
* User has clicked the learn more help link.
|
||||
*/
|
||||
data object LearnMoreClick : AutoFillAction()
|
||||
|
||||
/**
|
||||
* Internal actions.
|
||||
*/
|
||||
|
||||
@ -25,6 +25,7 @@ class AutoFillHandlers(
|
||||
val onAskToAddLoginClick: (isEnabled: Boolean) -> Unit,
|
||||
val onDefaultUriMatchTypeSelect: (defaultUriMatchType: UriMatchType) -> Unit,
|
||||
val onBlockAutoFillClick: () -> Unit,
|
||||
val onLearnMoreClick: () -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
@ -86,6 +87,7 @@ class AutoFillHandlers(
|
||||
)
|
||||
},
|
||||
onBlockAutoFillClick = { viewModel.trySendAction(AutoFillAction.BlockAutoFillClick) },
|
||||
onLearnMoreClick = { viewModel.trySendAction(AutoFillAction.LearnMoreClick) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,3 +31,15 @@ fun UriMatchType.toSdkUriMatchType(): com.bitwarden.vault.UriMatchType =
|
||||
UriMatchType.REGULAR_EXPRESSION -> com.bitwarden.vault.UriMatchType.REGULAR_EXPRESSION
|
||||
UriMatchType.STARTS_WITH -> com.bitwarden.vault.UriMatchType.STARTS_WITH
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the [UriMatchType] is considered an advanced matching strategy.
|
||||
*/
|
||||
fun UriMatchType.isAdvancedMatching(): Boolean =
|
||||
when (this) {
|
||||
UriMatchType.REGULAR_EXPRESSION,
|
||||
UriMatchType.STARTS_WITH,
|
||||
-> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ import com.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.ui.platform.components.coachmark.CoachMarkScope
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
@ -43,6 +44,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditSshKeyT
|
||||
fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
|
||||
state: VaultAddEditState.ViewState.Content,
|
||||
isAddItemMode: Boolean,
|
||||
defaultUriMatchType: UriMatchType,
|
||||
commonTypeHandlers: VaultAddEditCommonHandlers,
|
||||
loginItemTypeHandlers: VaultAddEditLoginTypeHandlers,
|
||||
identityItemTypeHandlers: VaultAddEditIdentityTypeHandlers,
|
||||
@ -212,6 +214,7 @@ fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
|
||||
onNextCoachMark = onNextCoachMark,
|
||||
onCoachMarkTourComplete = onCoachMarkTourComplete,
|
||||
onCoachMarkDismissed = onCoachMarkDismissed,
|
||||
defaultUriMatchType = defaultUriMatchType,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.ui.platform.components.coachmark.CoachMarkActionText
|
||||
import com.x8bit.bitwarden.ui.platform.components.coachmark.CoachMarkScope
|
||||
import com.x8bit.bitwarden.ui.platform.components.coachmark.model.CoachMarkHighlightShape
|
||||
@ -54,6 +55,7 @@ fun LazyListScope.vaultAddEditLoginItems(
|
||||
onPreviousCoachMark: () -> Unit,
|
||||
onCoachMarkTourComplete: () -> Unit,
|
||||
onCoachMarkDismissed: () -> Unit,
|
||||
defaultUriMatchType: UriMatchType,
|
||||
) = coachMarkScope.run {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
@ -192,7 +194,9 @@ fun LazyListScope.vaultAddEditLoginItems(
|
||||
uriItem = uriItem,
|
||||
onUriValueChange = loginItemTypeHandlers.onUriValueChange,
|
||||
onUriItemRemoved = loginItemTypeHandlers.onRemoveUriClick,
|
||||
onLearnMoreClick = loginItemTypeHandlers.onLearnMoreClick,
|
||||
cardStyle = cardStyle,
|
||||
defaultUriMatchType = defaultUriMatchType,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
@ -194,6 +194,10 @@ fun VaultAddEditScreen(
|
||||
}
|
||||
|
||||
is VaultAddEditEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
|
||||
|
||||
VaultAddEditEvent.NavigateToLearnMore -> {
|
||||
intentManager.launchUri("https://bitwarden.com/help/uri-match-detection/".toUri())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -412,6 +416,7 @@ fun VaultAddEditScreen(
|
||||
VaultAddEditContent(
|
||||
state = viewState,
|
||||
isAddItemMode = state.isAddItemMode,
|
||||
defaultUriMatchType = state.defaultUriMatchType,
|
||||
loginItemTypeHandlers = loginItemTypeHandlers,
|
||||
commonTypeHandlers = commonTypeHandlers,
|
||||
permissionsManager = permissionsManager,
|
||||
|
||||
@ -1,25 +1,38 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.addedit
|
||||
|
||||
import android.content.res.Resources
|
||||
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.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.bitwarden.core.util.persistentListOfNotNull
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenBasicDialogRow
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
|
||||
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectDialogContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.dropdown.model.MultiSelectOption
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
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.vault.feature.addedit.model.UriItem
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.displayLabel
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.isAdvancedMatching
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDisplayMatchType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toUriMatchType
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* The URI item displayed to the user.
|
||||
@ -31,10 +44,20 @@ fun VaultAddEditUriItem(
|
||||
onUriItemRemoved: (UriItem) -> Unit,
|
||||
onUriValueChange: (UriItem) -> Unit,
|
||||
cardStyle: CardStyle,
|
||||
defaultUriMatchType: UriMatchType,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
resources: Resources = LocalContext.current.resources,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
) {
|
||||
var shouldShowOptionsDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var shouldShowMatchDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var shouldShowAdvancedMatchDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var optionPendingConfirmation by rememberSaveable { mutableStateOf<UriMatchDisplayType?>(null) }
|
||||
var shouldShowLearnMoreMatchDetectionDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val defaultUriOption = remember(defaultUriMatchType) {
|
||||
defaultUriMatchType.displayLabel.toString(resources)
|
||||
}
|
||||
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.website_uri),
|
||||
@ -76,26 +99,173 @@ fun VaultAddEditUriItem(
|
||||
}
|
||||
|
||||
if (shouldShowMatchDialog) {
|
||||
val selectedString = uriItem.match.toDisplayMatchType().text.invoke()
|
||||
|
||||
BitwardenSelectionDialog(
|
||||
title = stringResource(id = BitwardenString.uri_match_detection),
|
||||
onDismissRequest = { shouldShowMatchDialog = false },
|
||||
) {
|
||||
UriMatchDisplayType
|
||||
.entries
|
||||
.forEach { matchType ->
|
||||
BitwardenSelectionRow(
|
||||
text = matchType.text,
|
||||
isSelected = matchType.text.invoke() == selectedString,
|
||||
onClick = {
|
||||
shouldShowMatchDialog = false
|
||||
onUriValueChange(
|
||||
uriItem.copy(match = matchType.toUriMatchType()),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
BitwardenMultiSelectDialogContent(
|
||||
options = uriMatchingOptions(defaultUriOption = defaultUriOption),
|
||||
selectedOption = MultiSelectOption.Row(
|
||||
uriItem
|
||||
.match
|
||||
.toDisplayMatchType()
|
||||
.displayLabel(defaultUriOption = defaultUriOption)
|
||||
.invoke(),
|
||||
),
|
||||
onOptionSelected = { selectedOption ->
|
||||
shouldShowMatchDialog = false
|
||||
|
||||
val newSelectedType =
|
||||
UriMatchDisplayType
|
||||
.entries
|
||||
.first {
|
||||
it.displayLabel(defaultUriOption)
|
||||
.invoke(resources) == selectedOption.title
|
||||
}
|
||||
|
||||
if (newSelectedType.isAdvancedMatching()) {
|
||||
optionPendingConfirmation = newSelectedType
|
||||
shouldShowAdvancedMatchDialog = true
|
||||
} else {
|
||||
onUriValueChange(
|
||||
uriItem.copy(match = newSelectedType.toUriMatchType()),
|
||||
)
|
||||
optionPendingConfirmation = null
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val currentOptionToConfirm = optionPendingConfirmation
|
||||
|
||||
if (shouldShowAdvancedMatchDialog && currentOptionToConfirm != null) {
|
||||
AdvancedMatchDetectionWarning(
|
||||
pendingOption = currentOptionToConfirm,
|
||||
defaultUriMatchType = defaultUriMatchType,
|
||||
onDialogConfirm = {
|
||||
onUriValueChange(
|
||||
uriItem.copy(match = currentOptionToConfirm.toUriMatchType()),
|
||||
)
|
||||
shouldShowAdvancedMatchDialog = false
|
||||
optionPendingConfirmation = null
|
||||
shouldShowLearnMoreMatchDetectionDialog = true
|
||||
},
|
||||
onDialogDismiss = {
|
||||
shouldShowAdvancedMatchDialog = false
|
||||
optionPendingConfirmation = null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (shouldShowLearnMoreMatchDetectionDialog) {
|
||||
LearnMoreAboutMatchDetectionDialog(
|
||||
uriMatchDisplayType = uriItem.match.toDisplayMatchType(),
|
||||
defaultUriOption = defaultUriOption,
|
||||
onDialogConfirm = {
|
||||
onLearnMoreClick()
|
||||
shouldShowLearnMoreMatchDetectionDialog = false
|
||||
},
|
||||
onDialogDismiss = {
|
||||
shouldShowLearnMoreMatchDetectionDialog = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AdvancedMatchDetectionWarning(
|
||||
pendingOption: UriMatchDisplayType,
|
||||
defaultUriMatchType: UriMatchType,
|
||||
onDialogConfirm: () -> Unit,
|
||||
onDialogDismiss: () -> Unit,
|
||||
) {
|
||||
|
||||
val descriptionStringResId = when (pendingOption) {
|
||||
UriMatchDisplayType.STARTS_WITH -> {
|
||||
BitwardenString.advanced_option_with_increased_risk_of_exposing_credentials
|
||||
}
|
||||
|
||||
UriMatchDisplayType.REGULAR_EXPRESSION -> {
|
||||
BitwardenString.advanced_option_increased_risk_exposing_credentials_used_incorrectly
|
||||
}
|
||||
|
||||
UriMatchDisplayType.DEFAULT,
|
||||
UriMatchDisplayType.HOST,
|
||||
UriMatchDisplayType.BASE_DOMAIN,
|
||||
UriMatchDisplayType.EXACT,
|
||||
UriMatchDisplayType.NEVER,
|
||||
->
|
||||
error("Unexpected option on AdvancedMatchDetectionWarning")
|
||||
}
|
||||
|
||||
val nameOfSelectedMatchDisplayType = pendingOption
|
||||
.displayLabel(defaultUriOption = defaultUriMatchType.displayLabel())
|
||||
.invoke()
|
||||
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(
|
||||
id = BitwardenString.are_you_sure_you_want_to_use,
|
||||
formatArgs = arrayOf(nameOfSelectedMatchDisplayType),
|
||||
),
|
||||
message = stringResource(
|
||||
id = descriptionStringResId,
|
||||
formatArgs = arrayOf(nameOfSelectedMatchDisplayType),
|
||||
),
|
||||
confirmButtonText = stringResource(id = BitwardenString.yes),
|
||||
dismissButtonText = stringResource(id = BitwardenString.close),
|
||||
onConfirmClick = onDialogConfirm,
|
||||
onDismissClick = onDialogDismiss,
|
||||
onDismissRequest = onDialogDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LearnMoreAboutMatchDetectionDialog(
|
||||
uriMatchDisplayType: UriMatchDisplayType,
|
||||
defaultUriOption: String,
|
||||
onDialogConfirm: () -> Unit,
|
||||
onDialogDismiss: () -> Unit,
|
||||
) {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = BitwardenString.keep_your_credential_secure),
|
||||
message = stringResource(
|
||||
id = BitwardenString.learn_more_about_how_to_keep_credentirals_secure,
|
||||
formatArgs = arrayOf(
|
||||
uriMatchDisplayType
|
||||
.displayLabel(
|
||||
defaultUriOption = defaultUriOption,
|
||||
)
|
||||
.invoke(),
|
||||
),
|
||||
),
|
||||
confirmButtonText = stringResource(id = BitwardenString.learn_more),
|
||||
dismissButtonText = stringResource(id = BitwardenString.cancel),
|
||||
onConfirmClick = onDialogConfirm,
|
||||
onDismissClick = onDialogDismiss,
|
||||
onDismissRequest = onDialogDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun uriMatchingOptions(defaultUriOption: String): ImmutableList<MultiSelectOption> {
|
||||
val advancedOptions = UriMatchDisplayType.entries.filter { it.isAdvancedMatching() }
|
||||
return persistentListOfNotNull(
|
||||
*UriMatchDisplayType
|
||||
.entries
|
||||
.filter { !it.isAdvancedMatching() }
|
||||
.map { MultiSelectOption.Row(it.displayLabel(defaultUriOption).invoke()) }
|
||||
.toTypedArray(),
|
||||
if (advancedOptions.isNotEmpty()) {
|
||||
MultiSelectOption.Header(
|
||||
title = stringResource(id = BitwardenString.advanced_options),
|
||||
testTag = "AdvancedOptionsSection",
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
*advancedOptions
|
||||
.map { MultiSelectOption.Row(it.displayLabel(defaultUriOption).invoke()) }
|
||||
.toTypedArray(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -42,6 +42,7 @@ import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrN
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toCreateCredentialRequestOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toTotpDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
@ -202,6 +203,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
shouldExitOnSave = shouldExitOnSave,
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldClearSpecialCircumstance = autofillSelectionData == null,
|
||||
defaultUriMatchType = settingsRepository.defaultUriMatchType,
|
||||
)
|
||||
},
|
||||
) {
|
||||
@ -1056,9 +1058,17 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick -> {
|
||||
handleAuthenticatorHelpToolTipClick()
|
||||
}
|
||||
|
||||
VaultAddEditAction.ItemType.LoginType.LearnMoreClick -> {
|
||||
handleLearnMoreClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLearnMoreClick() {
|
||||
sendEvent(VaultAddEditEvent.NavigateToLearnMore)
|
||||
}
|
||||
|
||||
private fun handleStartLearnAboutLogins() {
|
||||
coachMarkTourCompleted()
|
||||
sendEvent(VaultAddEditEvent.StartAddLoginItemCoachMarkTour)
|
||||
@ -2230,6 +2240,7 @@ data class VaultAddEditState(
|
||||
val shouldClearSpecialCircumstance: Boolean = true,
|
||||
val totpData: TotpData? = null,
|
||||
val createCredentialRequest: CreateCredentialRequest? = null,
|
||||
val defaultUriMatchType: UriMatchType,
|
||||
private val shouldShowCoachMarkTour: Boolean,
|
||||
) : Parcelable {
|
||||
|
||||
@ -2880,6 +2891,11 @@ sealed class VaultAddEditEvent {
|
||||
* Navigate the user to the tooltip URI for Authenticator key help.
|
||||
*/
|
||||
data object NavigateToAuthenticatorKeyTooltipUri : VaultAddEditEvent()
|
||||
|
||||
/**
|
||||
* Navigate the user to the learn more help page
|
||||
*/
|
||||
data object NavigateToLearnMore : VaultAddEditEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3216,6 +3232,11 @@ sealed class VaultAddEditAction {
|
||||
* User has clicked the call to action on the authenticator help tooltip.
|
||||
*/
|
||||
data object AuthenticatorHelpToolTipClick : LoginType()
|
||||
|
||||
/**
|
||||
* User has clicked the call to action on the learn more help link.
|
||||
*/
|
||||
data object LearnMoreClick : LoginType()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -47,6 +47,7 @@ data class VaultAddEditLoginTypeHandlers(
|
||||
val onStartLoginCoachMarkTour: () -> Unit,
|
||||
val onDismissLearnAboutLoginsCard: () -> Unit,
|
||||
val onAuthenticatorHelpToolTipClick: () -> Unit,
|
||||
val onLearnMoreClick: () -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
@ -143,6 +144,11 @@ data class VaultAddEditLoginTypeHandlers(
|
||||
VaultAddEditAction.ItemType.LoginType.LearnAboutLoginsDismissed,
|
||||
)
|
||||
},
|
||||
onLearnMoreClick = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.LoginType.LearnMoreClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,50 +1,44 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.addedit.model
|
||||
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
|
||||
/**
|
||||
* The options displayed to the user when choosing a match type
|
||||
* for their URI.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
enum class UriMatchDisplayType(
|
||||
val text: Text,
|
||||
) {
|
||||
enum class UriMatchDisplayType {
|
||||
/**
|
||||
* the default option for when the user has not chosen one.
|
||||
*/
|
||||
DEFAULT(BitwardenString.default_text.asText()),
|
||||
DEFAULT,
|
||||
|
||||
/**
|
||||
* The URIs match if their top-level and second-level domains match.
|
||||
*/
|
||||
BASE_DOMAIN(BitwardenString.base_domain.asText()),
|
||||
BASE_DOMAIN,
|
||||
|
||||
/**
|
||||
* The URIs match if their hostnames (and ports if specified) match.
|
||||
*/
|
||||
HOST(BitwardenString.host.asText()),
|
||||
HOST,
|
||||
|
||||
/**
|
||||
* The URIs match if the "test" URI starts with the known URI.
|
||||
*/
|
||||
STARTS_WITH(BitwardenString.starts_with.asText()),
|
||||
STARTS_WITH,
|
||||
|
||||
/**
|
||||
* The URIs match if the "test" URI matches the known URI according to a specified regular
|
||||
* expression for the item.
|
||||
*/
|
||||
REGULAR_EXPRESSION(BitwardenString.reg_ex.asText()),
|
||||
REGULAR_EXPRESSION,
|
||||
|
||||
/**
|
||||
* The URIs match if they are exactly the same.
|
||||
*/
|
||||
EXACT(BitwardenString.exact.asText()),
|
||||
EXACT,
|
||||
|
||||
/**
|
||||
* The URIs should never match.
|
||||
*/
|
||||
NEVER(BitwardenString.never.asText()),
|
||||
NEVER,
|
||||
}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.addedit.util
|
||||
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.UriMatchType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType
|
||||
|
||||
@ -30,3 +33,30 @@ fun UriMatchDisplayType.toUriMatchType(): UriMatchType? =
|
||||
UriMatchDisplayType.EXACT -> UriMatchType.EXACT
|
||||
UriMatchDisplayType.NEVER -> UriMatchType.NEVER
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the [UriMatchDisplayType] is considered an advanced matching strategy.
|
||||
*/
|
||||
fun UriMatchDisplayType.isAdvancedMatching(): Boolean =
|
||||
when (this) {
|
||||
UriMatchDisplayType.REGULAR_EXPRESSION,
|
||||
UriMatchDisplayType.STARTS_WITH,
|
||||
-> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable display label for the given [UriMatchType].
|
||||
*/
|
||||
fun UriMatchDisplayType.displayLabel(defaultUriOption: String): Text {
|
||||
return when (this) {
|
||||
UriMatchDisplayType.DEFAULT -> BitwardenString.default_text.asText(defaultUriOption)
|
||||
UriMatchDisplayType.BASE_DOMAIN -> BitwardenString.base_domain.asText()
|
||||
UriMatchDisplayType.HOST -> BitwardenString.host.asText()
|
||||
UriMatchDisplayType.STARTS_WITH -> BitwardenString.starts_with.asText()
|
||||
UriMatchDisplayType.REGULAR_EXPRESSION -> BitwardenString.reg_ex.asText()
|
||||
UriMatchDisplayType.EXACT -> BitwardenString.exact.asText()
|
||||
UriMatchDisplayType.NEVER -> BitwardenString.never.asText()
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.core.net.toUri
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.ui.util.assertNoDialogExists
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage
|
||||
@ -31,6 +32,7 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class AutoFillScreenTest : BitwardenComposeTest() {
|
||||
|
||||
private var isSystemSettingsRequestSuccess = false
|
||||
@ -50,6 +52,7 @@ class AutoFillScreenTest : BitwardenComposeTest() {
|
||||
every { startSystemAutofillSettingsActivity() } answers { isSystemSettingsRequestSuccess }
|
||||
every { startCredentialManagerSettings(any()) } just runs
|
||||
every { startSystemAccessibilitySettingsActivity() } just runs
|
||||
every { launchUri(any()) } just runs
|
||||
every { startBrowserAutofillSettingsActivity(any()) } returns true
|
||||
}
|
||||
|
||||
@ -672,6 +675,114 @@ class AutoFillScreenTest : BitwardenComposeTest() {
|
||||
viewModel.trySendAction(AutoFillAction.PrivilegedAppsClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on default URI match type dialog item click should send warning when is an Advanced Option`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Default URI match detection")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Starts with")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
"“Starts with” is an advanced option with " +
|
||||
"increased risk of exposing credentials.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on advanced match detection warning dialog click on cancel should not change the default URI match type`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Default URI match detection")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Starts with")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 0) {
|
||||
viewModel.trySendAction(
|
||||
AutoFillAction.DefaultUriMatchTypeSelect(
|
||||
defaultUriMatchType = UriMatchType.STARTS_WITH,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Advanced matching warning dialog confirm should display learn more dialog`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Default URI match detection")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Starts with")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Yes")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Keep your credentials secure")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on Advanced matching warning dialog click on more about match detection should call launchUri`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Default URI match detection")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Starts with")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Yes")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Learn more")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
AutoFillAction.LearnMoreClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateToLearnMore should call launchUri`() {
|
||||
mutableEventFlow.tryEmit(AutoFillEvent.NavigateToLearnMore)
|
||||
intentManager.launchUri("https://bitwarden.com/help/uri-match-detection/".toUri())
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE: AutoFillState = AutoFillState(
|
||||
|
||||
@ -450,6 +450,19 @@ class AutoFillViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when LearnMoreClick action is handled NavigateToLearnMore event is sent`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AutoFillAction.LearnMoreClick)
|
||||
assertEquals(
|
||||
AutoFillEvent.NavigateToLearnMore,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
state: AutoFillState? = DEFAULT_STATE,
|
||||
): AutoFillViewModel = AutoFillViewModel(
|
||||
|
||||
@ -52,4 +52,32 @@ class UriMatchTypeExtensionsTest {
|
||||
UriMatchType.STARTS_WITH.toSdkUriMatchType(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isAdvancedMatching should return the correct value for each type`() {
|
||||
assertEquals(
|
||||
false,
|
||||
UriMatchType.DOMAIN.isAdvancedMatching(),
|
||||
)
|
||||
assertEquals(
|
||||
false,
|
||||
UriMatchType.EXACT.isAdvancedMatching(),
|
||||
)
|
||||
assertEquals(
|
||||
false,
|
||||
UriMatchType.HOST.isAdvancedMatching(),
|
||||
)
|
||||
assertEquals(
|
||||
false,
|
||||
UriMatchType.NEVER.isAdvancedMatching(),
|
||||
)
|
||||
assertEquals(
|
||||
true,
|
||||
UriMatchType.REGULAR_EXPRESSION.isAdvancedMatching(),
|
||||
)
|
||||
assertEquals(
|
||||
true,
|
||||
UriMatchType.STARTS_WITH.isAdvancedMatching(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,6 +83,7 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType as UriMatchTypeModel
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
@ -1365,7 +1366,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Default")
|
||||
.onNodeWithText("Default (Exact)")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
|
||||
@ -1386,6 +1387,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Regular expression")
|
||||
.performScrollTo()
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
|
||||
@ -1477,6 +1479,186 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on match detection when an Advanced option is selected should display warning dialog when Regular Expression`() {
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Website (URI)")
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription(value = "Options"))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Match detection")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Regular expression")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
"“Regular expression” is an advanced option with " +
|
||||
"increased risk of exposing credentials if used incorrectly.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on match detection when an Advanced option is selected should display warning dialog when Starts With`() {
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Website (URI)")
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription(value = "Options"))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Match detection")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Starts with")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
"“Starts with” is an advanced option with " +
|
||||
"increased risk of exposing credentials.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on advanced match detection warning dialog click on cancel should not change the default URI match type`() {
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Website (URI)")
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription(value = "Options"))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Match detection")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Starts with")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Close")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 0) {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.LoginType.UriValueChange(
|
||||
UriItem(
|
||||
id = "TestId",
|
||||
uri = null,
|
||||
match = UriMatchType.REGULAR_EXPRESSION,
|
||||
checksum = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Advanced matching warning dialog confirm should display learn more dialog`() {
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateLoginType(currentState) {
|
||||
copy(
|
||||
uriList = listOf(
|
||||
UriItem(id = "TestId", uri = null, match = null, checksum = null),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Website (URI)")
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription(value = "Options"))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Match detection")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Starts with")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Yes")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Keep your credentials secure")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on Advanced matching warning dialog click on more about match detection should call launchUri`() {
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateLoginType(currentState) {
|
||||
copy(
|
||||
uriList = listOf(
|
||||
UriItem(id = "TestId", uri = null, match = null, checksum = null),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Website (URI)")
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription(value = "Options"))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Match detection")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Starts with")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Yes")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Learn more")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.LoginType.LearnMoreClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in ItemType_Login state clicking the New URI button should trigger AddNewUriClick`() {
|
||||
composeTestRule
|
||||
@ -3960,6 +4142,12 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateToLearnMore should call launchUri`() {
|
||||
mutableEventFlow.tryEmit(VaultAddEditEvent.NavigateToLearnMore)
|
||||
intentManager.launchUri("https://bitwarden.com/help/uri-match-detection/".toUri())
|
||||
}
|
||||
|
||||
//region Helper functions
|
||||
|
||||
private fun updateLoginType(
|
||||
@ -4086,6 +4274,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
bottomSheetState = null,
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
shouldShowCoachMarkTour = false,
|
||||
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_LOGIN = VaultAddEditState(
|
||||
@ -4099,6 +4288,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
dialog = null,
|
||||
bottomSheetState = null,
|
||||
shouldShowCoachMarkTour = false,
|
||||
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_IDENTITY = VaultAddEditState(
|
||||
@ -4112,6 +4302,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
dialog = null,
|
||||
bottomSheetState = null,
|
||||
shouldShowCoachMarkTour = false,
|
||||
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_CARD = VaultAddEditState(
|
||||
@ -4125,6 +4316,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
dialog = null,
|
||||
bottomSheetState = null,
|
||||
shouldShowCoachMarkTour = false,
|
||||
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_SECURE_NOTES_CUSTOM_FIELDS = VaultAddEditState(
|
||||
@ -4148,6 +4340,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
cipherType = VaultItemCipherType.SECURE_NOTE,
|
||||
shouldShowCoachMarkTour = false,
|
||||
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_SECURE_NOTES = VaultAddEditState(
|
||||
@ -4161,6 +4354,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
dialog = null,
|
||||
bottomSheetState = null,
|
||||
shouldShowCoachMarkTour = false,
|
||||
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_SSH_KEYS = VaultAddEditState(
|
||||
@ -4174,6 +4368,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
dialog = null,
|
||||
bottomSheetState = null,
|
||||
shouldShowCoachMarkTour = false,
|
||||
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||
)
|
||||
|
||||
private val ALTERED_COLLECTIONS = listOf(
|
||||
|
||||
@ -119,6 +119,7 @@ import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.util.UUID
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType as UriMatchTypeModel
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
@ -131,6 +132,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
every { initialAutofillDialogShown = any() } just runs
|
||||
every { initialAutofillDialogShown } returns true
|
||||
every { isUnlockWithPinEnabled } returns false
|
||||
every { defaultUriMatchType } returns UriMatchTypeModel.EXACT
|
||||
}
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(createUserState())
|
||||
private val authRepository: AuthRepository = mockk {
|
||||
@ -247,6 +249,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
shouldShowCloseButton = true,
|
||||
shouldExitOnSave = false,
|
||||
shouldShowCoachMarkTour = false,
|
||||
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||
)
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
@ -333,6 +336,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
dialog = null,
|
||||
bottomSheetState = null,
|
||||
shouldShowCoachMarkTour = false,
|
||||
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
@ -4568,6 +4572,19 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when LearnMoreClick action is handled NavigateToLearnMore event is sent`() =
|
||||
runTest {
|
||||
val viewModel = createAddVaultItemViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultAddEditAction.ItemType.LoginType.LearnMoreClick)
|
||||
assertEquals(
|
||||
VaultAddEditEvent.NavigateToLearnMore,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//region Helper functions
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ -4601,6 +4618,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldClearSpecialCircumstance = shouldClearSpecialCircumstance,
|
||||
createCredentialRequest = createCredentialRequest,
|
||||
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||
)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
||||
@ -286,7 +286,7 @@ Scanning will happen automatically.</string>
|
||||
<string name="new_uri">New URI</string>
|
||||
<string name="add_website">Add website</string>
|
||||
<string name="base_domain">Base domain</string>
|
||||
<string name="default_text">Default</string>
|
||||
<string name="default_text">Default (%1$s)</string>
|
||||
<string name="exact">Exact</string>
|
||||
<string name="host">Host</string>
|
||||
<string name="reg_ex">Regular expression</string>
|
||||
@ -356,7 +356,7 @@ Scanning will happen automatically.</string>
|
||||
<string name="clear_clipboard">Clear clipboard</string>
|
||||
<string name="clear_clipboard_description">Automatically clear copied values from your clipboard.</string>
|
||||
<string name="default_uri_match_detection">Default URI match detection</string>
|
||||
<string name="default_uri_match_detection_description">Choose the default way that URI match detection is handled for logins when performing actions such as autofill.</string>
|
||||
<string name="default_uri_match_detection_description">URI match detection controls how Bitwarden identifies autofill suggestions.</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="theme_description">Change the application\'s color theme</string>
|
||||
<string name="copy_notes">Copy note</string>
|
||||
@ -1069,4 +1069,13 @@ Do you want to switch to this account?</string>
|
||||
<string name="items_expanded_click_to_collapse">Items are expanded, click to collapse.</string>
|
||||
<string name="items_are_collapsed_click_to_expand">Items are collapsed, click to expand.</string>
|
||||
<string name="select_a_card_for_x">Select a card for %s</string>
|
||||
<string name="advanced_options">Advanced options</string>
|
||||
<string name="more_about_match_detection"><annotation link="moreAboutMatchDetection">More about match detection</annotation></string>
|
||||
<string name="keep_your_credential_secure">Keep your credentials secure</string>
|
||||
<string name="learn_more_about_how_to_keep_credentirals_secure">Learn more about how to keep credentials secure when using “%1$s”.</string>
|
||||
<string name="are_you_sure_you_want_to_use">Are you sure you want to use “%1$s”?</string>
|
||||
<string name="default_uri_match_detection_description_advanced_options_incorrectly">URI match detection controls how Bitwarden identifies autofill suggestions.\n<annotation emphasis="bold">Warning:</annotation> “Regular expression” is an advanced option with increased risk of exposing credentials if used incorrectly.</string>
|
||||
<string name="default_uri_match_detection_description_advanced_options">URI match detection controls how Bitwarden identifies autofill suggestions.\n<annotation emphasis="bold">Warning:</annotation> “Starts with” is an advanced option with increased risk of exposing credentials.</string>
|
||||
<string name="advanced_option_with_increased_risk_of_exposing_credentials">“Starts with” is an advanced option with increased risk of exposing credentials.</string>
|
||||
<string name="advanced_option_increased_risk_exposing_credentials_used_incorrectly">“Regular expression” is an advanced option with increased risk of exposing credentials if used incorrectly.</string>
|
||||
</resources>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user