mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 19:17:16 -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.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
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.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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.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.base.util.standardHorizontalMargin
|
||||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||||
import com.bitwarden.ui.platform.components.badge.NotificationBadge
|
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.components.util.rememberVectorPainter
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
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.data.platform.repository.model.UriMatchType
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
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.dialog.BitwardenTwoButtonDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
|
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.header.BitwardenListHeaderText
|
||||||
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
|
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
|
||||||
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow
|
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.browser.BrowserAutofillSettingsCard
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.handlers.AutoFillHandlers
|
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.displayLabel
|
||||||
|
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.util.isAdvancedMatching
|
||||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@ -109,6 +117,10 @@ fun AutoFillScreen(
|
|||||||
AutoFillEvent.NavigateToPrivilegedAppsListScreen -> {
|
AutoFillEvent.NavigateToPrivilegedAppsListScreen -> {
|
||||||
onNavigateToPrivilegedAppsList()
|
onNavigateToPrivilegedAppsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AutoFillEvent.NavigateToLearnMore -> {
|
||||||
|
intentManager.launchUri("https://bitwarden.com/help/uri-match-detection/".toUri())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,6 +315,7 @@ private fun AutoFillScreenContent(
|
|||||||
DefaultUriMatchTypeRow(
|
DefaultUriMatchTypeRow(
|
||||||
selectedUriMatchType = state.defaultUriMatchType,
|
selectedUriMatchType = state.defaultUriMatchType,
|
||||||
onUriMatchTypeSelect = autoFillHandlers.onDefaultUriMatchTypeSelect,
|
onUriMatchTypeSelect = autoFillHandlers.onDefaultUriMatchTypeSelect,
|
||||||
|
onNavigateToLearnMore = autoFillHandlers.onLearnMoreClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("DefaultUriMatchDetectionChooser")
|
.testTag("DefaultUriMatchDetectionChooser")
|
||||||
.standardHorizontalMargin()
|
.standardHorizontalMargin()
|
||||||
@ -387,24 +400,205 @@ private fun AccessibilityAutofillSwitch(
|
|||||||
private fun DefaultUriMatchTypeRow(
|
private fun DefaultUriMatchTypeRow(
|
||||||
selectedUriMatchType: UriMatchType,
|
selectedUriMatchType: UriMatchType,
|
||||||
onUriMatchTypeSelect: (UriMatchType) -> Unit,
|
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,
|
modifier: Modifier = Modifier,
|
||||||
resources: Resources = LocalContext.current.resources,
|
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(
|
BitwardenMultiSelectButton(
|
||||||
label = stringResource(id = BitwardenString.default_uri_match_detection),
|
label = stringResource(id = BitwardenString.default_uri_match_detection),
|
||||||
options = UriMatchType.entries.map { it.displayLabel() }.toImmutableList(),
|
options = options,
|
||||||
selectedOption = selectedUriMatchType.displayLabel(),
|
selectedOption = MultiSelectOption.Row(selectedUriMatchType.displayLabel()),
|
||||||
onOptionSelected = { selectedOption ->
|
onOptionSelected = { row ->
|
||||||
onUriMatchTypeSelect(
|
val newSelectedType = UriMatchType
|
||||||
UriMatchType
|
.entries
|
||||||
.entries
|
.first { it.displayLabel(resources) == row.title }
|
||||||
.first { it.displayLabel.toString(resources) == selectedOption },
|
onOptionSelected(newSelectedType)
|
||||||
)
|
|
||||||
},
|
},
|
||||||
supportingText = stringResource(
|
|
||||||
id = BitwardenString.default_uri_match_detection_description,
|
|
||||||
),
|
|
||||||
cardStyle = CardStyle.Full,
|
cardStyle = CardStyle.Full,
|
||||||
|
supportingContent = { SupportingTextForMatchDetection(selectedUriMatchType) },
|
||||||
modifier = modifier,
|
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)
|
is AutoFillAction.BrowserAutofillSelected -> handleBrowserAutofillSelected(action)
|
||||||
AutoFillAction.AboutPrivilegedAppsClick -> handleAboutPrivilegedAppsClick()
|
AutoFillAction.AboutPrivilegedAppsClick -> handleAboutPrivilegedAppsClick()
|
||||||
AutoFillAction.PrivilegedAppsClick -> handlePrivilegedAppsClick()
|
AutoFillAction.PrivilegedAppsClick -> handlePrivilegedAppsClick()
|
||||||
|
AutoFillAction.LearnMoreClick -> handleLearnMoreClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePrivilegedAppsClick() {
|
private fun handlePrivilegedAppsClick() {
|
||||||
sendEvent(AutoFillEvent.NavigateToPrivilegedAppsListScreen)
|
sendEvent(AutoFillEvent.NavigateToPrivilegedAppsListScreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleLearnMoreClick() {
|
||||||
|
sendEvent(AutoFillEvent.NavigateToLearnMore)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleInternalAction(action: AutoFillAction.Internal) {
|
private fun handleInternalAction(action: AutoFillAction.Internal) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is AutoFillAction.Internal.AccessibilityEnabledUpdateReceive -> {
|
is AutoFillAction.Internal.AccessibilityEnabledUpdateReceive -> {
|
||||||
@ -390,6 +395,11 @@ sealed class AutoFillEvent {
|
|||||||
* Navigate to the privileged apps list screen.
|
* Navigate to the privileged apps list screen.
|
||||||
*/
|
*/
|
||||||
data object NavigateToPrivilegedAppsListScreen : AutoFillEvent()
|
data object NavigateToPrivilegedAppsListScreen : AutoFillEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the learn more.
|
||||||
|
*/
|
||||||
|
data object NavigateToLearnMore : AutoFillEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -476,6 +486,11 @@ sealed class AutoFillAction {
|
|||||||
*/
|
*/
|
||||||
data object PrivilegedAppsClick : AutoFillAction()
|
data object PrivilegedAppsClick : AutoFillAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has clicked the learn more help link.
|
||||||
|
*/
|
||||||
|
data object LearnMoreClick : AutoFillAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal actions.
|
* Internal actions.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -25,6 +25,7 @@ class AutoFillHandlers(
|
|||||||
val onAskToAddLoginClick: (isEnabled: Boolean) -> Unit,
|
val onAskToAddLoginClick: (isEnabled: Boolean) -> Unit,
|
||||||
val onDefaultUriMatchTypeSelect: (defaultUriMatchType: UriMatchType) -> Unit,
|
val onDefaultUriMatchTypeSelect: (defaultUriMatchType: UriMatchType) -> Unit,
|
||||||
val onBlockAutoFillClick: () -> Unit,
|
val onBlockAutoFillClick: () -> Unit,
|
||||||
|
val onLearnMoreClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@Suppress("UndocumentedPublicClass")
|
@Suppress("UndocumentedPublicClass")
|
||||||
companion object {
|
companion object {
|
||||||
@ -86,6 +87,7 @@ class AutoFillHandlers(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onBlockAutoFillClick = { viewModel.trySendAction(AutoFillAction.BlockAutoFillClick) },
|
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.REGULAR_EXPRESSION -> com.bitwarden.vault.UriMatchType.REGULAR_EXPRESSION
|
||||||
UriMatchType.STARTS_WITH -> com.bitwarden.vault.UriMatchType.STARTS_WITH
|
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.components.model.CardStyle
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
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.coachmark.CoachMarkScope
|
||||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||||
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
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(
|
fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
|
||||||
state: VaultAddEditState.ViewState.Content,
|
state: VaultAddEditState.ViewState.Content,
|
||||||
isAddItemMode: Boolean,
|
isAddItemMode: Boolean,
|
||||||
|
defaultUriMatchType: UriMatchType,
|
||||||
commonTypeHandlers: VaultAddEditCommonHandlers,
|
commonTypeHandlers: VaultAddEditCommonHandlers,
|
||||||
loginItemTypeHandlers: VaultAddEditLoginTypeHandlers,
|
loginItemTypeHandlers: VaultAddEditLoginTypeHandlers,
|
||||||
identityItemTypeHandlers: VaultAddEditIdentityTypeHandlers,
|
identityItemTypeHandlers: VaultAddEditIdentityTypeHandlers,
|
||||||
@ -212,6 +214,7 @@ fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
|
|||||||
onNextCoachMark = onNextCoachMark,
|
onNextCoachMark = onNextCoachMark,
|
||||||
onCoachMarkTourComplete = onCoachMarkTourComplete,
|
onCoachMarkTourComplete = onCoachMarkTourComplete,
|
||||||
onCoachMarkDismissed = onCoachMarkDismissed,
|
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.platform.theme.BitwardenTheme
|
||||||
import com.bitwarden.ui.util.Text
|
import com.bitwarden.ui.util.Text
|
||||||
import com.bitwarden.ui.util.asText
|
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.CoachMarkActionText
|
||||||
import com.x8bit.bitwarden.ui.platform.components.coachmark.CoachMarkScope
|
import com.x8bit.bitwarden.ui.platform.components.coachmark.CoachMarkScope
|
||||||
import com.x8bit.bitwarden.ui.platform.components.coachmark.model.CoachMarkHighlightShape
|
import com.x8bit.bitwarden.ui.platform.components.coachmark.model.CoachMarkHighlightShape
|
||||||
@ -54,6 +55,7 @@ fun LazyListScope.vaultAddEditLoginItems(
|
|||||||
onPreviousCoachMark: () -> Unit,
|
onPreviousCoachMark: () -> Unit,
|
||||||
onCoachMarkTourComplete: () -> Unit,
|
onCoachMarkTourComplete: () -> Unit,
|
||||||
onCoachMarkDismissed: () -> Unit,
|
onCoachMarkDismissed: () -> Unit,
|
||||||
|
defaultUriMatchType: UriMatchType,
|
||||||
) = coachMarkScope.run {
|
) = coachMarkScope.run {
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||||
@ -192,7 +194,9 @@ fun LazyListScope.vaultAddEditLoginItems(
|
|||||||
uriItem = uriItem,
|
uriItem = uriItem,
|
||||||
onUriValueChange = loginItemTypeHandlers.onUriValueChange,
|
onUriValueChange = loginItemTypeHandlers.onUriValueChange,
|
||||||
onUriItemRemoved = loginItemTypeHandlers.onRemoveUriClick,
|
onUriItemRemoved = loginItemTypeHandlers.onRemoveUriClick,
|
||||||
|
onLearnMoreClick = loginItemTypeHandlers.onLearnMoreClick,
|
||||||
cardStyle = cardStyle,
|
cardStyle = cardStyle,
|
||||||
|
defaultUriMatchType = defaultUriMatchType,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -194,6 +194,10 @@ fun VaultAddEditScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is VaultAddEditEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
|
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(
|
VaultAddEditContent(
|
||||||
state = viewState,
|
state = viewState,
|
||||||
isAddItemMode = state.isAddItemMode,
|
isAddItemMode = state.isAddItemMode,
|
||||||
|
defaultUriMatchType = state.defaultUriMatchType,
|
||||||
loginItemTypeHandlers = loginItemTypeHandlers,
|
loginItemTypeHandlers = loginItemTypeHandlers,
|
||||||
commonTypeHandlers = commonTypeHandlers,
|
commonTypeHandlers = commonTypeHandlers,
|
||||||
permissionsManager = permissionsManager,
|
permissionsManager = permissionsManager,
|
||||||
|
|||||||
@ -1,25 +1,38 @@
|
|||||||
package com.x8bit.bitwarden.ui.vault.feature.addedit
|
package com.x8bit.bitwarden.ui.vault.feature.addedit
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
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.button.BitwardenStandardIconButton
|
||||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
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.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.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.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.UriItem
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType
|
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.toDisplayMatchType
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toUriMatchType
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toUriMatchType
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URI item displayed to the user.
|
* The URI item displayed to the user.
|
||||||
@ -31,10 +44,20 @@ fun VaultAddEditUriItem(
|
|||||||
onUriItemRemoved: (UriItem) -> Unit,
|
onUriItemRemoved: (UriItem) -> Unit,
|
||||||
onUriValueChange: (UriItem) -> Unit,
|
onUriValueChange: (UriItem) -> Unit,
|
||||||
cardStyle: CardStyle,
|
cardStyle: CardStyle,
|
||||||
|
defaultUriMatchType: UriMatchType,
|
||||||
|
onLearnMoreClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
resources: Resources = LocalContext.current.resources,
|
||||||
|
intentManager: IntentManager = LocalIntentManager.current,
|
||||||
) {
|
) {
|
||||||
var shouldShowOptionsDialog by rememberSaveable { mutableStateOf(false) }
|
var shouldShowOptionsDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
var shouldShowMatchDialog 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(
|
BitwardenTextField(
|
||||||
label = stringResource(id = BitwardenString.website_uri),
|
label = stringResource(id = BitwardenString.website_uri),
|
||||||
@ -76,26 +99,173 @@ fun VaultAddEditUriItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shouldShowMatchDialog) {
|
if (shouldShowMatchDialog) {
|
||||||
val selectedString = uriItem.match.toDisplayMatchType().text.invoke()
|
|
||||||
|
|
||||||
BitwardenSelectionDialog(
|
BitwardenSelectionDialog(
|
||||||
title = stringResource(id = BitwardenString.uri_match_detection),
|
title = stringResource(id = BitwardenString.uri_match_detection),
|
||||||
onDismissRequest = { shouldShowMatchDialog = false },
|
onDismissRequest = { shouldShowMatchDialog = false },
|
||||||
) {
|
) {
|
||||||
UriMatchDisplayType
|
BitwardenMultiSelectDialogContent(
|
||||||
.entries
|
options = uriMatchingOptions(defaultUriOption = defaultUriOption),
|
||||||
.forEach { matchType ->
|
selectedOption = MultiSelectOption.Row(
|
||||||
BitwardenSelectionRow(
|
uriItem
|
||||||
text = matchType.text,
|
.match
|
||||||
isSelected = matchType.text.invoke() == selectedString,
|
.toDisplayMatchType()
|
||||||
onClick = {
|
.displayLabel(defaultUriOption = defaultUriOption)
|
||||||
shouldShowMatchDialog = false
|
.invoke(),
|
||||||
onUriValueChange(
|
),
|
||||||
uriItem.copy(match = matchType.toUriMatchType()),
|
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.toCreateCredentialRequestOrNull
|
||||||
import com.x8bit.bitwarden.data.platform.manager.util.toTotpDataOrNull
|
import com.x8bit.bitwarden.data.platform.manager.util.toTotpDataOrNull
|
||||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
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.GeneratorRepository
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
||||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||||
@ -202,6 +203,7 @@ class VaultAddEditViewModel @Inject constructor(
|
|||||||
shouldExitOnSave = shouldExitOnSave,
|
shouldExitOnSave = shouldExitOnSave,
|
||||||
shouldShowCoachMarkTour = false,
|
shouldShowCoachMarkTour = false,
|
||||||
shouldClearSpecialCircumstance = autofillSelectionData == null,
|
shouldClearSpecialCircumstance = autofillSelectionData == null,
|
||||||
|
defaultUriMatchType = settingsRepository.defaultUriMatchType,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@ -1056,9 +1058,17 @@ class VaultAddEditViewModel @Inject constructor(
|
|||||||
VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick -> {
|
VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick -> {
|
||||||
handleAuthenticatorHelpToolTipClick()
|
handleAuthenticatorHelpToolTipClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VaultAddEditAction.ItemType.LoginType.LearnMoreClick -> {
|
||||||
|
handleLearnMoreClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleLearnMoreClick() {
|
||||||
|
sendEvent(VaultAddEditEvent.NavigateToLearnMore)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleStartLearnAboutLogins() {
|
private fun handleStartLearnAboutLogins() {
|
||||||
coachMarkTourCompleted()
|
coachMarkTourCompleted()
|
||||||
sendEvent(VaultAddEditEvent.StartAddLoginItemCoachMarkTour)
|
sendEvent(VaultAddEditEvent.StartAddLoginItemCoachMarkTour)
|
||||||
@ -2230,6 +2240,7 @@ data class VaultAddEditState(
|
|||||||
val shouldClearSpecialCircumstance: Boolean = true,
|
val shouldClearSpecialCircumstance: Boolean = true,
|
||||||
val totpData: TotpData? = null,
|
val totpData: TotpData? = null,
|
||||||
val createCredentialRequest: CreateCredentialRequest? = null,
|
val createCredentialRequest: CreateCredentialRequest? = null,
|
||||||
|
val defaultUriMatchType: UriMatchType,
|
||||||
private val shouldShowCoachMarkTour: Boolean,
|
private val shouldShowCoachMarkTour: Boolean,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
@ -2880,6 +2891,11 @@ sealed class VaultAddEditEvent {
|
|||||||
* Navigate the user to the tooltip URI for Authenticator key help.
|
* Navigate the user to the tooltip URI for Authenticator key help.
|
||||||
*/
|
*/
|
||||||
data object NavigateToAuthenticatorKeyTooltipUri : VaultAddEditEvent()
|
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.
|
* User has clicked the call to action on the authenticator help tooltip.
|
||||||
*/
|
*/
|
||||||
data object AuthenticatorHelpToolTipClick : LoginType()
|
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 onStartLoginCoachMarkTour: () -> Unit,
|
||||||
val onDismissLearnAboutLoginsCard: () -> Unit,
|
val onDismissLearnAboutLoginsCard: () -> Unit,
|
||||||
val onAuthenticatorHelpToolTipClick: () -> Unit,
|
val onAuthenticatorHelpToolTipClick: () -> Unit,
|
||||||
|
val onLearnMoreClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@Suppress("UndocumentedPublicClass")
|
@Suppress("UndocumentedPublicClass")
|
||||||
companion object {
|
companion object {
|
||||||
@ -143,6 +144,11 @@ data class VaultAddEditLoginTypeHandlers(
|
|||||||
VaultAddEditAction.ItemType.LoginType.LearnAboutLoginsDismissed,
|
VaultAddEditAction.ItemType.LoginType.LearnAboutLoginsDismissed,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onLearnMoreClick = {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultAddEditAction.ItemType.LoginType.LearnMoreClick,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,50 +1,44 @@
|
|||||||
package com.x8bit.bitwarden.ui.vault.feature.addedit.model
|
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
|
* The options displayed to the user when choosing a match type
|
||||||
* for their URI.
|
* for their URI.
|
||||||
*/
|
*/
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
enum class UriMatchDisplayType(
|
enum class UriMatchDisplayType {
|
||||||
val text: Text,
|
|
||||||
) {
|
|
||||||
/**
|
/**
|
||||||
* the default option for when the user has not chosen one.
|
* 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.
|
* 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.
|
* 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.
|
* 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
|
* The URIs match if the "test" URI matches the known URI according to a specified regular
|
||||||
* expression for the item.
|
* expression for the item.
|
||||||
*/
|
*/
|
||||||
REGULAR_EXPRESSION(BitwardenString.reg_ex.asText()),
|
REGULAR_EXPRESSION,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URIs match if they are exactly the same.
|
* The URIs match if they are exactly the same.
|
||||||
*/
|
*/
|
||||||
EXACT(BitwardenString.exact.asText()),
|
EXACT,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URIs should never match.
|
* The URIs should never match.
|
||||||
*/
|
*/
|
||||||
NEVER(BitwardenString.never.asText()),
|
NEVER,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
package com.x8bit.bitwarden.ui.vault.feature.addedit.util
|
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.bitwarden.vault.UriMatchType
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType
|
||||||
|
|
||||||
@ -30,3 +33,30 @@ fun UriMatchDisplayType.toUriMatchType(): UriMatchType? =
|
|||||||
UriMatchDisplayType.EXACT -> UriMatchType.EXACT
|
UriMatchDisplayType.EXACT -> UriMatchType.EXACT
|
||||||
UriMatchDisplayType.NEVER -> UriMatchType.NEVER
|
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.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.compose.ui.test.performScrollTo
|
import androidx.compose.ui.test.performScrollTo
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||||
import com.bitwarden.ui.util.assertNoDialogExists
|
import com.bitwarden.ui.util.assertNoDialogExists
|
||||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage
|
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.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
@Suppress("LargeClass")
|
||||||
class AutoFillScreenTest : BitwardenComposeTest() {
|
class AutoFillScreenTest : BitwardenComposeTest() {
|
||||||
|
|
||||||
private var isSystemSettingsRequestSuccess = false
|
private var isSystemSettingsRequestSuccess = false
|
||||||
@ -50,6 +52,7 @@ class AutoFillScreenTest : BitwardenComposeTest() {
|
|||||||
every { startSystemAutofillSettingsActivity() } answers { isSystemSettingsRequestSuccess }
|
every { startSystemAutofillSettingsActivity() } answers { isSystemSettingsRequestSuccess }
|
||||||
every { startCredentialManagerSettings(any()) } just runs
|
every { startCredentialManagerSettings(any()) } just runs
|
||||||
every { startSystemAccessibilitySettingsActivity() } just runs
|
every { startSystemAccessibilitySettingsActivity() } just runs
|
||||||
|
every { launchUri(any()) } just runs
|
||||||
every { startBrowserAutofillSettingsActivity(any()) } returns true
|
every { startBrowserAutofillSettingsActivity(any()) } returns true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -672,6 +675,114 @@ class AutoFillScreenTest : BitwardenComposeTest() {
|
|||||||
viewModel.trySendAction(AutoFillAction.PrivilegedAppsClick)
|
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(
|
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(
|
private fun createViewModel(
|
||||||
state: AutoFillState? = DEFAULT_STATE,
|
state: AutoFillState? = DEFAULT_STATE,
|
||||||
): AutoFillViewModel = AutoFillViewModel(
|
): AutoFillViewModel = AutoFillViewModel(
|
||||||
|
|||||||
@ -52,4 +52,32 @@ class UriMatchTypeExtensionsTest {
|
|||||||
UriMatchType.STARTS_WITH.toSdkUriMatchType(),
|
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.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType as UriMatchTypeModel
|
||||||
|
|
||||||
@Suppress("LargeClass")
|
@Suppress("LargeClass")
|
||||||
class VaultAddEditScreenTest : BitwardenComposeTest() {
|
class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||||
@ -1365,7 +1366,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
|||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Default")
|
.onNodeWithText("Default (Exact)")
|
||||||
.assert(hasAnyAncestor(isDialog()))
|
.assert(hasAnyAncestor(isDialog()))
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
|
|
||||||
@ -1386,6 +1387,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
|||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Regular expression")
|
.onNodeWithText("Regular expression")
|
||||||
|
.performScrollTo()
|
||||||
.assert(hasAnyAncestor(isDialog()))
|
.assert(hasAnyAncestor(isDialog()))
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
|
|
||||||
@ -1477,6 +1479,186 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
|||||||
composeTestRule.assertNoDialogExists()
|
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
|
@Test
|
||||||
fun `in ItemType_Login state clicking the New URI button should trigger AddNewUriClick`() {
|
fun `in ItemType_Login state clicking the New URI button should trigger AddNewUriClick`() {
|
||||||
composeTestRule
|
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
|
//region Helper functions
|
||||||
|
|
||||||
private fun updateLoginType(
|
private fun updateLoginType(
|
||||||
@ -4086,6 +4274,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
|||||||
bottomSheetState = null,
|
bottomSheetState = null,
|
||||||
vaultAddEditType = VaultAddEditType.AddItem,
|
vaultAddEditType = VaultAddEditType.AddItem,
|
||||||
shouldShowCoachMarkTour = false,
|
shouldShowCoachMarkTour = false,
|
||||||
|
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_STATE_LOGIN = VaultAddEditState(
|
private val DEFAULT_STATE_LOGIN = VaultAddEditState(
|
||||||
@ -4099,6 +4288,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
|||||||
dialog = null,
|
dialog = null,
|
||||||
bottomSheetState = null,
|
bottomSheetState = null,
|
||||||
shouldShowCoachMarkTour = false,
|
shouldShowCoachMarkTour = false,
|
||||||
|
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_STATE_IDENTITY = VaultAddEditState(
|
private val DEFAULT_STATE_IDENTITY = VaultAddEditState(
|
||||||
@ -4112,6 +4302,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
|||||||
dialog = null,
|
dialog = null,
|
||||||
bottomSheetState = null,
|
bottomSheetState = null,
|
||||||
shouldShowCoachMarkTour = false,
|
shouldShowCoachMarkTour = false,
|
||||||
|
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_STATE_CARD = VaultAddEditState(
|
private val DEFAULT_STATE_CARD = VaultAddEditState(
|
||||||
@ -4125,6 +4316,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
|||||||
dialog = null,
|
dialog = null,
|
||||||
bottomSheetState = null,
|
bottomSheetState = null,
|
||||||
shouldShowCoachMarkTour = false,
|
shouldShowCoachMarkTour = false,
|
||||||
|
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_STATE_SECURE_NOTES_CUSTOM_FIELDS = VaultAddEditState(
|
private val DEFAULT_STATE_SECURE_NOTES_CUSTOM_FIELDS = VaultAddEditState(
|
||||||
@ -4148,6 +4340,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
|||||||
vaultAddEditType = VaultAddEditType.AddItem,
|
vaultAddEditType = VaultAddEditType.AddItem,
|
||||||
cipherType = VaultItemCipherType.SECURE_NOTE,
|
cipherType = VaultItemCipherType.SECURE_NOTE,
|
||||||
shouldShowCoachMarkTour = false,
|
shouldShowCoachMarkTour = false,
|
||||||
|
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_STATE_SECURE_NOTES = VaultAddEditState(
|
private val DEFAULT_STATE_SECURE_NOTES = VaultAddEditState(
|
||||||
@ -4161,6 +4354,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
|||||||
dialog = null,
|
dialog = null,
|
||||||
bottomSheetState = null,
|
bottomSheetState = null,
|
||||||
shouldShowCoachMarkTour = false,
|
shouldShowCoachMarkTour = false,
|
||||||
|
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_STATE_SSH_KEYS = VaultAddEditState(
|
private val DEFAULT_STATE_SSH_KEYS = VaultAddEditState(
|
||||||
@ -4174,6 +4368,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
|||||||
dialog = null,
|
dialog = null,
|
||||||
bottomSheetState = null,
|
bottomSheetState = null,
|
||||||
shouldShowCoachMarkTour = false,
|
shouldShowCoachMarkTour = false,
|
||||||
|
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val ALTERED_COLLECTIONS = listOf(
|
private val ALTERED_COLLECTIONS = listOf(
|
||||||
|
|||||||
@ -119,6 +119,7 @@ import java.time.Clock
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType as UriMatchTypeModel
|
||||||
|
|
||||||
@Suppress("LargeClass")
|
@Suppress("LargeClass")
|
||||||
class VaultAddEditViewModelTest : BaseViewModelTest() {
|
class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
@ -131,6 +132,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||||||
every { initialAutofillDialogShown = any() } just runs
|
every { initialAutofillDialogShown = any() } just runs
|
||||||
every { initialAutofillDialogShown } returns true
|
every { initialAutofillDialogShown } returns true
|
||||||
every { isUnlockWithPinEnabled } returns false
|
every { isUnlockWithPinEnabled } returns false
|
||||||
|
every { defaultUriMatchType } returns UriMatchTypeModel.EXACT
|
||||||
}
|
}
|
||||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(createUserState())
|
private val mutableUserStateFlow = MutableStateFlow<UserState?>(createUserState())
|
||||||
private val authRepository: AuthRepository = mockk {
|
private val authRepository: AuthRepository = mockk {
|
||||||
@ -247,6 +249,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||||||
shouldShowCloseButton = true,
|
shouldShowCloseButton = true,
|
||||||
shouldExitOnSave = false,
|
shouldExitOnSave = false,
|
||||||
shouldShowCoachMarkTour = false,
|
shouldShowCoachMarkTour = false,
|
||||||
|
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||||
)
|
)
|
||||||
val viewModel = createAddVaultItemViewModel(
|
val viewModel = createAddVaultItemViewModel(
|
||||||
savedStateHandle = createSavedStateHandleWithState(
|
savedStateHandle = createSavedStateHandleWithState(
|
||||||
@ -333,6 +336,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||||||
dialog = null,
|
dialog = null,
|
||||||
bottomSheetState = null,
|
bottomSheetState = null,
|
||||||
shouldShowCoachMarkTour = false,
|
shouldShowCoachMarkTour = false,
|
||||||
|
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
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
|
//region Helper functions
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
@ -4601,6 +4618,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||||||
shouldShowCoachMarkTour = false,
|
shouldShowCoachMarkTour = false,
|
||||||
shouldClearSpecialCircumstance = shouldClearSpecialCircumstance,
|
shouldClearSpecialCircumstance = shouldClearSpecialCircumstance,
|
||||||
createCredentialRequest = createCredentialRequest,
|
createCredentialRequest = createCredentialRequest,
|
||||||
|
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
|
|||||||
@ -286,7 +286,7 @@ Scanning will happen automatically.</string>
|
|||||||
<string name="new_uri">New URI</string>
|
<string name="new_uri">New URI</string>
|
||||||
<string name="add_website">Add website</string>
|
<string name="add_website">Add website</string>
|
||||||
<string name="base_domain">Base domain</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="exact">Exact</string>
|
||||||
<string name="host">Host</string>
|
<string name="host">Host</string>
|
||||||
<string name="reg_ex">Regular expression</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">Clear clipboard</string>
|
||||||
<string name="clear_clipboard_description">Automatically clear copied values from your 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">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">Theme</string>
|
||||||
<string name="theme_description">Change the application\'s color theme</string>
|
<string name="theme_description">Change the application\'s color theme</string>
|
||||||
<string name="copy_notes">Copy note</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_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="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="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>
|
</resources>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user