Skip to content

Commit 4b1db96

Browse files
authored
Merge pull request #7005 from woocommerce/issue/6845-coupon-email-validation
Coupons: Add validation for allowed emails
2 parents 6fe3414 + b008f92 commit 4b1db96

File tree

6 files changed

+117
-4
lines changed

6 files changed

+117
-4
lines changed

WooCommerce/Classes/ViewRelated/Coupons/Add and Edit Coupons/UsageDetails/CouponAllowedEmails.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@ import SwiftUI
33
/// View to input allowed email formats for coupons
44
///
55
struct CouponAllowedEmails: View {
6-
@Binding var emailFormats: String
6+
@ObservedObject private var viewModel: CouponAllowedEmailsViewModel
7+
@Environment(\.presentationMode) var presentation
8+
9+
init(viewModel: CouponAllowedEmailsViewModel) {
10+
self.viewModel = viewModel
11+
}
712

813
var body: some View {
914
GeometryReader { geometry in
1015
VStack(alignment: .leading) {
11-
TextField("", text: $emailFormats)
16+
TextField("", text: $viewModel.emailPatterns)
1217
.labelsHidden()
1318
.padding(.horizontal, Constants.margin)
1419
.padding(.horizontal, insets: geometry.safeAreaInsets)
@@ -25,6 +30,17 @@ struct CouponAllowedEmails: View {
2530
}
2631
.navigationTitle(Localization.title)
2732
.navigationBarTitleDisplayMode(.inline)
33+
.toolbar {
34+
ToolbarItem(placement: .navigationBarTrailing) {
35+
Button(Localization.done) {
36+
viewModel.validateEmails {
37+
presentation.wrappedValue.dismiss()
38+
}
39+
}
40+
}
41+
}
42+
.notice($viewModel.notice)
43+
.wooNavigationBarStyle()
2844
}
2945
}
3046

@@ -41,11 +57,13 @@ private extension CouponAllowedEmails {
4157
"Separate email addresses with commas. You can also use an asterisk (*) " +
4258
"to match parts of an email. For example \"*@gmail.com\" would match all gmail addresses.",
4359
comment: "Description of the allowed emails field for coupons")
60+
static let done = NSLocalizedString("Done", comment: "Done button on the Allowed Emails screen")
4461
}
4562
}
4663

4764
struct CouponAllowedEmails_Previews: PreviewProvider {
4865
static var previews: some View {
49-
CouponAllowedEmails(emailFormats: .constant("*gmail.com, *@me.com"))
66+
let viewModel = CouponAllowedEmailsViewModel(allowedEmails: "*gmail.com, *@me.com") { _ in }
67+
CouponAllowedEmails(viewModel: viewModel)
5068
}
5169
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Foundation
2+
import WordPressShared
3+
4+
/// View model for `CouponAllowedEmails` view.
5+
///
6+
final class CouponAllowedEmailsViewModel: ObservableObject {
7+
8+
@Published var emailPatterns: String
9+
10+
/// Defines the current notice that should be shown.
11+
/// Defaults to `nil`.
12+
///
13+
@Published var notice: Notice?
14+
15+
private let onCompletion: (String) -> Void
16+
17+
init(allowedEmails: String, onCompletion: @escaping (String) -> Void) {
18+
self.emailPatterns = allowedEmails
19+
self.onCompletion = onCompletion
20+
}
21+
22+
/// Validate the input
23+
///
24+
func validateEmails(dismissHandler: @escaping () -> Void) {
25+
let emails = emailPatterns.components(separatedBy: ", ")
26+
let foundInvalidPatterns = emails.contains(where: { !EmailFormatValidator.validate(string: $0) })
27+
if !foundInvalidPatterns {
28+
onCompletion(emailPatterns)
29+
dismissHandler()
30+
} else {
31+
notice = Notice(title: Localization.failedEmailValidation, feedbackType: .error)
32+
}
33+
}
34+
}
35+
36+
private extension CouponAllowedEmailsViewModel {
37+
enum Localization {
38+
static let failedEmailValidation = NSLocalizedString(
39+
"Some email address is not valid.",
40+
comment: "Error message when at least an address on the Coupon Allowed Emails screen is not valid."
41+
)
42+
}
43+
}

WooCommerce/Classes/ViewRelated/Coupons/Add and Edit Coupons/UsageDetails/CouponRestrictions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ struct CouponRestrictions: View {
167167
categoryListConfig: categoryListConfig,
168168
viewModel: viewModel.categorySelectorViewModel)
169169
}
170-
LazyNavigationLink(destination: CouponAllowedEmails(emailFormats: $viewModel.allowedEmails), isActive: $showingAllowedEmails) {
170+
LazyNavigationLink(destination: CouponAllowedEmails(viewModel: viewModel.allowedEmailsViewModel), isActive: $showingAllowedEmails) {
171171
EmptyView()
172172
}
173173
}

WooCommerce/Classes/ViewRelated/Coupons/Add and Edit Coupons/UsageDetails/CouponRestrictionsViewModel.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ final class CouponRestrictionsViewModel: ObservableObject {
7575
}
7676
}()
7777

78+
lazy var allowedEmailsViewModel = {
79+
CouponAllowedEmailsViewModel(allowedEmails: allowedEmails) { [weak self] updatedEmails in
80+
self?.allowedEmails = updatedEmails
81+
}
82+
}()
83+
7884
private let siteID: Int64
7985
private let stores: StoresManager
8086
private let storageManager: StorageManagerType

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,6 +1574,7 @@
15741574
DE279BAD26E9CBEA002BA963 /* ShippingLabelPackagesFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE279BAC26E9CBEA002BA963 /* ShippingLabelPackagesFormViewModelTests.swift */; };
15751575
DE279BAF26EA03EA002BA963 /* ShippingLabelSinglePackageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE279BAE26EA03EA002BA963 /* ShippingLabelSinglePackageViewModelTests.swift */; };
15761576
DE279BB126EA184A002BA963 /* ShippingLabelPackageListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE279BB026EA184A002BA963 /* ShippingLabelPackageListViewModel.swift */; };
1577+
DE2BF4FD2846192B00FBE68A /* CouponAllowedEmailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2BF4FC2846192B00FBE68A /* CouponAllowedEmailsViewModelTests.swift */; };
15771578
DE34771327F174C8009CA300 /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34771227F174C8009CA300 /* StatusView.swift */; };
15781579
DE3877E0283B68CF0075D87E /* DiscountTypeBottomSheetListSelectorCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3877DF283B68CF0075D87E /* DiscountTypeBottomSheetListSelectorCommand.swift */; };
15791580
DE3877E2283CCBC20075D87E /* BottomSheetListSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3877E1283CCBC20075D87E /* BottomSheetListSelector.swift */; };
@@ -1633,6 +1634,7 @@
16331634
DECE13FB27993F6500816ECD /* TitleAndSubtitleAndStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DECE13F927993F6500816ECD /* TitleAndSubtitleAndStatusTableViewCell.swift */; };
16341635
DECE13FC27993F6500816ECD /* TitleAndSubtitleAndStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DECE13FA27993F6500816ECD /* TitleAndSubtitleAndStatusTableViewCell.xib */; };
16351636
DECE1400279A595200816ECD /* Coupon+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DECE13FF279A595200816ECD /* Coupon+Woo.swift */; };
1637+
DEDB2D262845D31900CE7D35 /* CouponAllowedEmailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEDB2D252845D31900CE7D35 /* CouponAllowedEmailsViewModel.swift */; };
16361638
DEDB886B26E8531E00981595 /* ShippingLabelPackageAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEDB886A26E8531E00981595 /* ShippingLabelPackageAttributes.swift */; };
16371639
DEE6437626D87C4100888A75 /* PrintCustomsFormsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE6437526D87C4100888A75 /* PrintCustomsFormsView.swift */; };
16381640
DEE6437826D8DAD900888A75 /* InProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE6437726D8DAD900888A75 /* InProgressView.swift */; };
@@ -3347,6 +3349,7 @@
33473349
DE279BAC26E9CBEA002BA963 /* ShippingLabelPackagesFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPackagesFormViewModelTests.swift; sourceTree = "<group>"; };
33483350
DE279BAE26EA03EA002BA963 /* ShippingLabelSinglePackageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelSinglePackageViewModelTests.swift; sourceTree = "<group>"; };
33493351
DE279BB026EA184A002BA963 /* ShippingLabelPackageListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPackageListViewModel.swift; sourceTree = "<group>"; };
3352+
DE2BF4FC2846192B00FBE68A /* CouponAllowedEmailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponAllowedEmailsViewModelTests.swift; sourceTree = "<group>"; };
33503353
DE34771227F174C8009CA300 /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
33513354
DE3877DF283B68CF0075D87E /* DiscountTypeBottomSheetListSelectorCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscountTypeBottomSheetListSelectorCommand.swift; sourceTree = "<group>"; };
33523355
DE3877E1283CCBC20075D87E /* BottomSheetListSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetListSelector.swift; sourceTree = "<group>"; };
@@ -3406,6 +3409,7 @@
34063409
DECE13F927993F6500816ECD /* TitleAndSubtitleAndStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleAndSubtitleAndStatusTableViewCell.swift; sourceTree = "<group>"; };
34073410
DECE13FA27993F6500816ECD /* TitleAndSubtitleAndStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TitleAndSubtitleAndStatusTableViewCell.xib; sourceTree = "<group>"; };
34083411
DECE13FF279A595200816ECD /* Coupon+Woo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Coupon+Woo.swift"; sourceTree = "<group>"; };
3412+
DEDB2D252845D31900CE7D35 /* CouponAllowedEmailsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CouponAllowedEmailsViewModel.swift; sourceTree = "<group>"; };
34093413
DEDB886A26E8531E00981595 /* ShippingLabelPackageAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPackageAttributes.swift; sourceTree = "<group>"; };
34103414
DEE6437526D87C4100888A75 /* PrintCustomsFormsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintCustomsFormsView.swift; sourceTree = "<group>"; };
34113415
DEE6437726D8DAD900888A75 /* InProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InProgressView.swift; sourceTree = "<group>"; };
@@ -4540,6 +4544,7 @@
45404544
4535EE7F281BE4E0004212B4 /* CouponAmountInputFormatterTests.swift */,
45414545
4535EE81281BE726004212B4 /* CouponCodeInputFormatterTests.swift */,
45424546
DE3877E3283E35E80075D87E /* DiscountTypeBottomSheetListSelectorCommandTests.swift */,
4547+
DE2BF4FC2846192B00FBE68A /* CouponAllowedEmailsViewModelTests.swift */,
45434548
);
45444549
path = Coupons;
45454550
sourceTree = "<group>";
@@ -7799,6 +7804,7 @@
77997804
DE69C54C27BB719A000BB888 /* CouponRestrictions.swift */,
78007805
DE69C54927BB715D000BB888 /* CouponRestrictionsViewModel.swift */,
78017806
DEC1508127F450AC00F4487C /* CouponAllowedEmails.swift */,
7807+
DEDB2D252845D31900CE7D35 /* CouponAllowedEmailsViewModel.swift */,
78027808
);
78037809
path = UsageDetails;
78047810
sourceTree = "<group>";
@@ -9522,6 +9528,7 @@
95229528
DE26B52C277DA11800A2EA0A /* CouponListView.swift in Sources */,
95239529
AEA622B2274669D3002A9B57 /* AddOrderCoordinator.swift in Sources */,
95249530
4535EE7A281ADD56004212B4 /* CouponCodeInputFormatter.swift in Sources */,
9531+
DEDB2D262845D31900CE7D35 /* CouponAllowedEmailsViewModel.swift in Sources */,
95259532
0282DD96233C960C006A5FDB /* SearchResultCell.swift in Sources */,
95269533
260C32BE2527A2DE00157BC2 /* IssueRefundViewModel.swift in Sources */,
95279534
2678897C270E6E8B00BD249E /* SimplePaymentsAmount.swift in Sources */,
@@ -9741,6 +9748,7 @@
97419748
E17E3BF9266917C10009D977 /* CardPresentModalScanningFailedTests.swift in Sources */,
97429749
269098B627D2C09D001FEB07 /* ShippingInputTransformerTests.swift in Sources */,
97439750
02BA128B24616B48008D8325 /* ProductFormActionsFactory+VisibilityTests.swift in Sources */,
9751+
DE2BF4FD2846192B00FBE68A /* CouponAllowedEmailsViewModelTests.swift in Sources */,
97449752
FEEB2F6E268A2F7B0075A6E0 /* RoleEligibilityUseCaseTests.swift in Sources */,
97459753
31E906A326CC91A70099A985 /* CardReaderConnectionControllerTests.swift in Sources */,
97469754
02AB40822784297C00929CF3 /* ProductTableViewCellViewModelTests.swift in Sources */,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import XCTest
2+
@testable import WooCommerce
3+
4+
final class CouponAllowedEmailsViewModelTests: XCTestCase {
5+
6+
func test_completion_block_is_triggered_when_address_validation_succeeds() {
7+
// Given
8+
var savedAddresses: String?
9+
let completionBlock: (String) -> Void = { email in
10+
savedAddresses = email
11+
}
12+
let viewModel = CouponAllowedEmailsViewModel(allowedEmails: "", onCompletion: completionBlock)
13+
14+
// When
15+
viewModel.emailPatterns = "*@mail.com"
16+
viewModel.validateEmails {}
17+
18+
// Then
19+
XCTAssertEqual(savedAddresses, "*@mail.com")
20+
XCTAssertNil(viewModel.notice)
21+
}
22+
23+
func test_completion_block_is_not_triggered_and_notice_is_not_nil_when_address_validation_fails() {
24+
var savedAddresses: String?
25+
let completionBlock: (String) -> Void = { email in
26+
savedAddresses = email
27+
}
28+
let viewModel = CouponAllowedEmailsViewModel(allowedEmails: "", onCompletion: completionBlock)
29+
30+
// When
31+
viewModel.emailPatterns = "*@mail"
32+
viewModel.validateEmails {}
33+
34+
// Then
35+
XCTAssertNil(savedAddresses)
36+
XCTAssertNotNil(viewModel.notice)
37+
}
38+
}

0 commit comments

Comments
 (0)