[PM-23723] URI Matching detection layout updates on advanced options (#5574)

This commit is contained in:
aj-rosado 2025-08-13 17:09:29 +01:00 committed by GitHub
parent 3ed63ef5eb
commit a688693f43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 875 additions and 45 deletions

View File

@ -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(),
)
}

View File

@ -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.
*/

View File

@ -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) },
)
}
}

View File

@ -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
}

View File

@ -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,
)
}

View File

@ -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(),
)

View File

@ -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,

View File

@ -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(),
)
}

View File

@ -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()
}
/**

View File

@ -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,
)
},
)
}
}

View File

@ -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,
}

View File

@ -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()
}
}

View File

@ -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(

View File

@ -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(

View File

@ -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(),
)
}
}

View File

@ -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(

View File

@ -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")

View File

@ -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>