Skip to content

Commit 713f8b5

Browse files
[PM-23721] Match detection update (#1887)
Co-authored-by: André Bispo <[email protected]>
1 parent 86b4dc7 commit 713f8b5

31 files changed

+670
-40
lines changed

BitwardenResources/Localizations/en.lproj/Localizable.strings

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,3 +1278,14 @@
12781278
"WhenUsingTwoStepVerification" = "When using 2-step verification, you’ll enter your username and password and a code generated in this app.";
12791279
"YesSetDefault" = "Yes, set default";
12801280
"YouCanUpdateYourDefaultAnytimeInSettings" = "You can update your default anytime in settings.";
1281+
"UriMatchDetectionControlsHowBitwardenIdentifiesAutofillSuggestions" = "URI match detection controls how Bitwarden identifies autofill suggestions.";
1282+
"StartsWithIsAnAdvancedOptionWithIncreasedRiskOfExposingCredentials" = "\"Starts with\” is an advanced option with increased risk of exposing credentials.";
1283+
"RegularExpressionIsAnAdvancedOptionWithIncreasedRiskOfExposingCredentials" = "\"Regular expression\” is an advanced option with increased risk of exposing credentials if used incorrectly.";
1284+
"StartsWithAdvanced" = "Starts with (advanced)";
1285+
"RegularExpressionAdvanced" = "Regular expression (advanced)";
1286+
"AreYouSureYouWantToUseX" = "Are you sure you want to use “%1$@”?";
1287+
"KeepYourCredentialsSecure" = "Keep your credentials secure";
1288+
"LearnMoreAboutHowToKeepCredentialsSecureWhenUsingX" = "Learn more about how to keep credentials secure when using “%1$@”.";
1289+
"WarningStartsWithIsAnAdvancedOptionWithIncreasedRiskOfExposingCredentials" = "**Warning:** \"Starts with\” is an advanced option with increased risk of exposing credentials.";
1290+
"WarningRegularExpressionIsAnAdvancedOptionWithIncreasedRiskOfExposingCredentials" = "**Warning:** \"Regular expression\” is an advanced option with increased risk of exposing credentials if used incorrectly.";
1291+
"DefaultX" = "Default (%1$@)";

BitwardenShared/Core/Platform/Utilities/ExternalLinksConstants.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,7 @@ enum ExternalLinksConstants {
5959

6060
/// A link to Bitwarden's help page for showing website icons.
6161
static let websiteIconsHelp = URL(string: "https://bitwarden.com/help/website-icons/")!
62+
63+
/// A link to Bitwarden's help page for URI match detection.
64+
static let uriMatchDetections = URL(string: "https://bitwarden.com/help/uri-match-detection/")!
6265
}

BitwardenShared/Core/Vault/Models/Enum/UriMatchType.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ enum UriMatchType: Int, CaseIterable, Codable, Equatable, Hashable, Menuable {
2525
switch self {
2626
case .domain: Localizations.baseDomain
2727
case .host: Localizations.host
28-
case .startsWith: Localizations.startsWith
2928
case .exact: Localizations.exact
30-
case .regularExpression: Localizations.regEx
3129
case .never: Localizations.never
30+
case .startsWith: Localizations.startsWithAdvanced
31+
case .regularExpression: Localizations.regularExpressionAdvanced
3232
}
3333
}
3434
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import BitwardenResources
2+
import SwiftUI
3+
4+
// MARK: - BitwardenMenuFooterTextModifier
5+
6+
/// A modifier for the footer on BitwardenMenu
7+
///
8+
struct BitwardenMenuFooterTextModifier: ViewModifier {
9+
/// The bottom padding of the modifier.
10+
var topPadding: CGFloat
11+
12+
/// The bottom padding of the modifier.
13+
var bottomPadding: CGFloat
14+
15+
// MARK: View
16+
17+
func body(content: Content) -> some View {
18+
content
19+
.styleGuide(.footnote, includeLinePadding: false, includeLineSpacing: false)
20+
.foregroundColor(SharedAsset.Colors.textSecondary.swiftUIColor)
21+
.multilineTextAlignment(.leading)
22+
.padding(.top, topPadding)
23+
.padding(.bottom, bottomPadding)
24+
}
25+
}
26+
27+
extension View {
28+
func bitwardenMenuFooterText(topPadding: CGFloat = 0, bottomPadding: CGFloat = 12) -> some View {
29+
modifier(BitwardenMenuFooterTextModifier(topPadding: topPadding, bottomPadding: bottomPadding))
30+
}
31+
}

BitwardenShared/UI/Platform/Application/Views/BitwardenMenuField.swift

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ struct BitwardenMenuField<
3737
T,
3838
AdditionalMenu: View,
3939
TitleAccessory: View,
40-
TrailingContent: View
40+
TrailingContent: View,
41+
FooterContent: View
4142
>: View where T: Menuable {
4243
// MARK: Properties
4344

@@ -59,8 +60,8 @@ struct BitwardenMenuField<
5960
/// The options displayed in the menu.
6061
let options: [T]
6162

62-
/// The footer text displayed below the menu field.
63-
let footer: String?
63+
/// The (optional) footer content to display underneath the field.
64+
var footerContent: FooterContent?
6465

6566
/// The title of the menu field.
6667
let title: String?
@@ -77,15 +78,7 @@ struct BitwardenMenuField<
7778
VStack(alignment: .leading, spacing: 0) {
7879
menu
7980

80-
if let footer {
81-
Divider()
82-
83-
Text(footer)
84-
.styleGuide(.footnote, includeLinePadding: false, includeLineSpacing: false)
85-
.foregroundColor(SharedAsset.Colors.textSecondary.swiftUIColor)
86-
.multilineTextAlignment(.leading)
87-
.padding(.vertical, 12)
88-
}
81+
footerView()
8982
}
9083
.padding(.horizontal, 16)
9184
.background(
@@ -190,15 +183,45 @@ struct BitwardenMenuField<
190183
accessibilityIdentifier: String? = nil,
191184
options: [T],
192185
selection: Binding<T>
186+
)
187+
where AdditionalMenu == EmptyView,
188+
TitleAccessory == EmptyView,
189+
TrailingContent == EmptyView,
190+
FooterContent == Text {
191+
self.accessibilityIdentifier = accessibilityIdentifier
192+
additionalMenu = nil
193+
footerContent = footer.map { footerText in Text(footerText) }
194+
self.options = options
195+
_selection = selection
196+
self.title = title
197+
trailingContent = nil
198+
titleAccessoryContent = nil
199+
}
200+
201+
/// Initializes a new `BitwardenMenuField`.
202+
///
203+
/// - Parameters:
204+
/// - title: The title of the text field.
205+
/// - footerView: The footer view displayed below the menu field.
206+
/// - accessibilityIdentifier: The accessibility identifier for the view.
207+
/// - options: The options that the user can choose between.
208+
/// - selection: A `Binding` for the currently selected option.
209+
///
210+
init(
211+
title: String,
212+
accessibilityIdentifier: String? = nil,
213+
options: [T],
214+
selection: Binding<T>,
215+
@ViewBuilder footer footerContent: () -> FooterContent
193216
) where AdditionalMenu == EmptyView, TitleAccessory == EmptyView, TrailingContent == EmptyView {
194217
self.accessibilityIdentifier = accessibilityIdentifier
195218
additionalMenu = nil
196-
self.footer = footer
197219
self.options = options
198220
_selection = selection
199221
self.title = title
200222
trailingContent = nil
201223
titleAccessoryContent = nil
224+
self.footerContent = footerContent()
202225
}
203226

204227
/// Initializes a new `BitwardenMenuField`.
@@ -220,10 +243,10 @@ struct BitwardenMenuField<
220243
selection: Binding<T>,
221244
titleAccessoryContent: () -> TitleAccessory,
222245
trailingContent: () -> TrailingContent
223-
) where AdditionalMenu == EmptyView {
246+
) where AdditionalMenu == EmptyView, FooterContent == Text {
224247
self.accessibilityIdentifier = accessibilityIdentifier
225248
additionalMenu = nil
226-
self.footer = footer
249+
footerContent = footer.map { footerText in Text(footerText) }
227250
self.options = options
228251
_selection = selection
229252
self.title = title
@@ -248,10 +271,10 @@ struct BitwardenMenuField<
248271
options: [T],
249272
selection: Binding<T>,
250273
trailingContent: () -> TrailingContent
251-
) where AdditionalMenu == EmptyView, TitleAccessory == EmptyView {
274+
) where AdditionalMenu == EmptyView, TitleAccessory == EmptyView, FooterContent == Text {
252275
self.accessibilityIdentifier = accessibilityIdentifier
253276
additionalMenu = nil
254-
self.footer = footer
277+
footerContent = footer.map { footerText in Text(footerText) }
255278
self.options = options
256279
_selection = selection
257280
self.title = title
@@ -276,10 +299,10 @@ struct BitwardenMenuField<
276299
options: [T],
277300
selection: Binding<T>,
278301
titleAccessoryContent: () -> TitleAccessory
279-
) where AdditionalMenu == EmptyView, TrailingContent == EmptyView {
302+
) where AdditionalMenu == EmptyView, TrailingContent == EmptyView, FooterContent == Text {
280303
self.accessibilityIdentifier = accessibilityIdentifier
281304
additionalMenu = nil
282-
self.footer = footer
305+
footerContent = footer.map { footerText in Text(footerText) }
283306
self.options = options
284307
_selection = selection
285308
self.title = title
@@ -305,16 +328,31 @@ struct BitwardenMenuField<
305328
options: [T],
306329
selection: Binding<T>,
307330
@ViewBuilder additionalMenu: () -> AdditionalMenu
308-
) where TrailingContent == EmptyView, TitleAccessory == EmptyView {
331+
) where TrailingContent == EmptyView, TitleAccessory == EmptyView, FooterContent == Text {
309332
self.accessibilityIdentifier = accessibilityIdentifier
310333
self.additionalMenu = additionalMenu()
311-
self.footer = footer
334+
footerContent = footer.map { footerText in Text(footerText) }
312335
self.options = options
313336
_selection = selection
314337
self.title = title
315338
titleAccessoryContent = nil
316339
trailingContent = nil
317340
}
341+
342+
/// The view to display at the footer below the main content.
343+
@ViewBuilder
344+
private func footerView() -> some View {
345+
if let footerContent {
346+
Group {
347+
Divider()
348+
if let footerContent = footerContent as? Text {
349+
footerContent.bitwardenMenuFooterText(topPadding: 12, bottomPadding: 12)
350+
} else {
351+
footerContent
352+
}
353+
}
354+
}
355+
}
318356
}
319357

320358
// MARK: Previews
@@ -336,13 +374,15 @@ private enum MenuPreviewOptions: CaseIterable, Menuable {
336374
VStack {
337375
BitwardenMenuField(
338376
title: "Animals",
377+
footer: nil,
339378
options: MenuPreviewOptions.allCases,
340379
selection: .constant(.dog)
341380
)
342381
.padding()
343382

344383
BitwardenMenuField(
345384
title: "Animals",
385+
footer: nil,
346386
options: MenuPreviewOptions.allCases,
347387
selection: .constant(.dog)
348388
)
@@ -398,3 +438,5 @@ private enum MenuPreviewOptions: CaseIterable, Menuable {
398438
.background(Color(.systemGroupedBackground))
399439
}
400440
#endif
441+
442+
// swiftlint:disable:this file_length

BitwardenShared/UI/Platform/Application/Views/BitwardenMenuFieldTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class BitwardenMenuFieldTests: BitwardenTestCase {
1919
// MARK: Properties
2020

2121
var selection: TestValue!
22-
var subject: BitwardenMenuField<TestValue, EmptyView, EmptyView, EmptyView>!
22+
var subject: BitwardenMenuField<TestValue, EmptyView, EmptyView, EmptyView, Text>!
2323

2424
// MARK: Setup & Teardown
2525

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// MARK: - Alert+Account
2+
3+
import BitwardenResources
4+
5+
extension Alert {
6+
7+
// MARK: Methods
8+
9+
/// Creates an alert asking if the user wants to use an advanced matching detection option.
10+
///
11+
/// - Parameters:
12+
/// - action: The action to perform if the user selects "Yes".
13+
/// - Returns: An alert prompting the user about advanced matching detection.
14+
static func confirmRegularExpressionMatchDetectionAlert(
15+
action: @escaping () async -> Void
16+
) -> Alert {
17+
Alert(
18+
title: Localizations.areYouSureYouWantToUseX(Localizations.regEx),
19+
message: Localizations.regularExpressionIsAnAdvancedOptionWithIncreasedRiskOfExposingCredentials,
20+
alertActions: [
21+
AlertAction(title: Localizations.cancel, style: .cancel),
22+
AlertAction(title: Localizations.yes, style: .default) { _ in
23+
await action()
24+
},
25+
]
26+
)
27+
}
28+
29+
/// Creates an alert asking if the user wants to use an advanced matching detection option.
30+
///
31+
/// - Parameters:
32+
/// - action: The action to perform if the user selects "Yes".
33+
/// - Returns: An alert prompting the user about advanced matching detection.
34+
static func confirmStartsWithMatchDetectionAlert(
35+
action: @escaping () async -> Void
36+
) -> Alert {
37+
Alert(
38+
title: Localizations.areYouSureYouWantToUseX(Localizations.startsWith),
39+
message: Localizations.startsWithIsAnAdvancedOptionWithIncreasedRiskOfExposingCredentials,
40+
alertActions: [
41+
AlertAction(title: Localizations.cancel, style: .cancel),
42+
AlertAction(title: Localizations.yes, style: .default) { _ in
43+
await action()
44+
},
45+
]
46+
)
47+
}
48+
49+
/// Creates an alert asking if the user wants to learn more about advanced matching detection options.
50+
///
51+
/// - Parameters:
52+
/// - matchingType: The type of matching option to learn more about.
53+
/// - action: The action to perform if the user selects "Learn More".
54+
/// - Returns: An alert prompting the user with additional information about advanced matching detection.
55+
static func learnMoreAdvancedMatchingDetection(
56+
_ matchingType: String,
57+
action: @escaping () async -> Void
58+
) -> Alert {
59+
Alert(
60+
title: Localizations.keepYourCredentialsSecure,
61+
message: Localizations.learnMoreAboutHowToKeepCredentialsSecureWhenUsingX(matchingType),
62+
alertActions: [
63+
AlertAction(title: Localizations.close, style: .cancel),
64+
AlertAction(title: Localizations.learnMore, style: .default) { _ in
65+
await action()
66+
},
67+
]
68+
)
69+
}
70+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import BitwardenResources
2+
import XCTest
3+
4+
@testable import BitwardenShared
5+
6+
class AlertPlatformTests: BitwardenTestCase {
7+
/// `confirmRegularExpressionMatchDetectionAlert(action:)` constructs an `Alert`
8+
/// with the correct title, message, and Cancel and Yes buttons.
9+
func test_confirmRegularExpressionMatchDetectionAlert() {
10+
let subject = Alert.confirmRegularExpressionMatchDetectionAlert {}
11+
12+
XCTAssertEqual(subject.preferredStyle, .alert)
13+
XCTAssertEqual(subject.title, Localizations.areYouSureYouWantToUseX(Localizations.regEx))
14+
XCTAssertEqual(subject.message, Localizations.regularExpressionIsAnAdvancedOptionWithIncreasedRiskOfExposingCredentials)
15+
XCTAssertEqual(subject.alertActions.count, 2)
16+
XCTAssertEqual(subject.alertActions.first?.title, Localizations.cancel)
17+
XCTAssertEqual(subject.alertActions.first?.style, .cancel)
18+
XCTAssertEqual(subject.alertActions.last?.title, Localizations.yes)
19+
XCTAssertEqual(subject.alertActions.last?.style, .default)
20+
}
21+
22+
/// `confirmStartsWithMatchDetectionAlert(action:)` constructs an `Alert`
23+
/// with the correct title, message, and Cancel and Yes buttons.
24+
func test_confirmStartsWithMatchDetectionAlert() {
25+
let subject = Alert.confirmStartsWithMatchDetectionAlert {}
26+
27+
XCTAssertEqual(subject.preferredStyle, .alert)
28+
XCTAssertEqual(subject.title, Localizations.areYouSureYouWantToUseX(Localizations.startsWith))
29+
XCTAssertEqual(
30+
subject.message,
31+
Localizations.startsWithIsAnAdvancedOptionWithIncreasedRiskOfExposingCredentials
32+
)
33+
XCTAssertEqual(subject.alertActions.count, 2)
34+
XCTAssertEqual(subject.alertActions.first?.title, Localizations.cancel)
35+
XCTAssertEqual(subject.alertActions.first?.style, .cancel)
36+
XCTAssertEqual(subject.alertActions.last?.title, Localizations.yes)
37+
XCTAssertEqual(subject.alertActions.last?.style, .default)
38+
}
39+
40+
/// `learnMoreAdvancedMatchingDetection(matchingType: action:)` constructs an `Alert`
41+
/// with the correct title, message, and Cancel and Yes buttons.
42+
func test_learnMoreAdvancedMatchingDetection() {
43+
let subject = Alert.learnMoreAdvancedMatchingDetection(UriMatchType.regularExpression.localizedName) {}
44+
45+
XCTAssertEqual(subject.preferredStyle, .alert)
46+
XCTAssertEqual(subject.title, Localizations.keepYourCredentialsSecure)
47+
XCTAssertEqual(
48+
subject.message,
49+
Localizations.learnMoreAboutHowToKeepCredentialsSecureWhenUsingX(
50+
UriMatchType.regularExpression.localizedName
51+
)
52+
)
53+
XCTAssertEqual(subject.alertActions.count, 2)
54+
XCTAssertEqual(subject.alertActions.first?.title, Localizations.close)
55+
XCTAssertEqual(subject.alertActions.first?.style, .cancel)
56+
XCTAssertEqual(subject.alertActions.last?.title, Localizations.learnMore)
57+
XCTAssertEqual(subject.alertActions.last?.style, .default)
58+
}
59+
}

BitwardenShared/UI/Platform/Settings/Settings/AutoFill/AutoFillAction.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ enum AutoFillAction: Equatable {
66
/// The app extension button was tapped.
77
case appExtensionTapped
88

9+
/// Clears the URL.
10+
case clearUrl
11+
912
/// The default URI match type was changed.
1013
case defaultUriMatchTypeChanged(UriMatchType)
1114

0 commit comments

Comments
 (0)