Skip to content

Commit 95e7e6c

Browse files
authored
Merge pull request #6542 from woocommerce/issue/6491-usage-restriction-update
Coupons: Add usage restrictions view
2 parents 5804f4b + f33bc8e commit 95e7e6c

File tree

7 files changed

+182
-71
lines changed

7 files changed

+182
-71
lines changed

WooCommerce/Classes/ViewRelated/Coupons/CouponDetails/CouponDetails.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ struct CouponDetails: View {
2727
@ObservedObject private var viewModel: CouponDetailsViewModel
2828
@State private var showingActionSheet: Bool = false
2929
@State private var showingShareSheet: Bool = false
30-
@State private var showingUsageDetails: Bool = false
3130
@State private var showingEditCoupon: Bool = false
3231
@State private var showingAmountLoadingErrorPrompt: Bool = false
3332
@State private var showingEnableAnalytics: Bool = false
@@ -177,9 +176,6 @@ struct CouponDetails: View {
177176

178177
Divider()
179178
}
180-
NavigationLink(destination: CouponUsageDetails(viewModel: .init(coupon: viewModel.coupon)), isActive: $showingUsageDetails) {
181-
EmptyView()
182-
}.hidden()
183179
}
184180
.background(Color(.listBackground))
185181
.ignoresSafeArea(.container, edges: [.horizontal])
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import SwiftUI
2+
3+
/// View to input allowed email formats for coupons
4+
///
5+
struct CouponAllowedEmails: View {
6+
@Binding var emailFormats: String
7+
8+
var body: some View {
9+
GeometryReader { geometry in
10+
VStack(alignment: .leading) {
11+
TextField("", text: $emailFormats)
12+
.labelsHidden()
13+
.padding(.horizontal, Constants.margin)
14+
.padding(.horizontal, insets: geometry.safeAreaInsets)
15+
Divider()
16+
.padding(.leading, Constants.margin)
17+
.padding(.leading, insets: geometry.safeAreaInsets)
18+
Text(Localization.description)
19+
.footnoteStyle()
20+
.padding(.horizontal, Constants.margin)
21+
.padding(.horizontal, insets: geometry.safeAreaInsets)
22+
}
23+
.padding(.top, Constants.topSpacing)
24+
.ignoresSafeArea(.container, edges: [.horizontal])
25+
}
26+
.navigationTitle(Localization.title)
27+
.navigationBarTitleDisplayMode(.inline)
28+
}
29+
}
30+
31+
private extension CouponAllowedEmails {
32+
enum Constants {
33+
static let margin: CGFloat = 16
34+
static let topSpacing: CGFloat = 24
35+
}
36+
37+
enum Localization {
38+
static let title = NSLocalizedString("Allowed Emails", comment: "Title for the Allowed Emails screen")
39+
static let description = NSLocalizedString(
40+
"List of allowed billing emails to check against when an order is placed. " +
41+
"Separate email addresses with commas. You can also use an asterisk (*) " +
42+
"to match parts of an email. For example \"*@gmail.com\" would match all gmail addresses.",
43+
comment: "Description of the allowed emails field for coupons")
44+
}
45+
}
46+
47+
struct CouponAllowedEmails_Previews: PreviewProvider {
48+
static var previews: some View {
49+
CouponAllowedEmails(emailFormats: .constant("*gmail.com, *@me.com"))
50+
}
51+
}

WooCommerce/Classes/ViewRelated/Coupons/UsageDetails/CouponUsageDetails.swift renamed to WooCommerce/Classes/ViewRelated/Coupons/UsageDetails/CouponRestrictions.swift

Lines changed: 106 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import SwiftUI
22

3-
struct CouponUsageDetails: View {
3+
struct CouponRestrictions: View {
44

5-
@ObservedObject private var viewModel: CouponUsageDetailsViewModel
5+
@State private var showingAllowedEmails: Bool = false
6+
@ObservedObject private var viewModel: CouponRestrictionsViewModel
67

7-
init(viewModel: CouponUsageDetailsViewModel) {
8+
// Tracks the scale of the view due to accessibility changes
9+
@ScaledMetric private var scale: CGFloat = 1.0
10+
11+
init(viewModel: CouponRestrictionsViewModel) {
812
self.viewModel = viewModel
913
}
1014

1115
var body: some View {
1216
GeometryReader { geometry in
1317
ScrollView {
1418
VStack(alignment: .leading, spacing: 0) {
15-
ListHeaderView(text: Localization.usageRestriction.uppercased(), alignment: .left)
16-
.padding(.horizontal, insets: geometry.safeAreaInsets)
1719
VStack(alignment: .leading, spacing: 0) {
18-
Divider()
1920
TitleAndTextFieldRow(title: String.localizedStringWithFormat(Localization.minimumSpend, viewModel.currencySymbol),
2021
placeholder: Localization.none,
2122
text: $viewModel.minimumSpend,
22-
editable: false,
2323
keyboardType: .decimalPad)
2424
.padding(.horizontal, insets: geometry.safeAreaInsets)
2525
Divider()
@@ -28,7 +28,6 @@ struct CouponUsageDetails: View {
2828
TitleAndTextFieldRow(title: String.localizedStringWithFormat(Localization.maximumSpend, viewModel.currencySymbol),
2929
placeholder: Localization.none,
3030
text: $viewModel.maximumSpend,
31-
editable: false,
3231
keyboardType: .decimalPad)
3332
.padding(.horizontal, insets: geometry.safeAreaInsets)
3433
Divider()
@@ -37,7 +36,6 @@ struct CouponUsageDetails: View {
3736
TitleAndTextFieldRow(title: Localization.usageLimitPerCoupon,
3837
placeholder: Localization.unlimited,
3938
text: $viewModel.usageLimitPerCoupon,
40-
editable: false,
4139
keyboardType: .asciiCapableNumberPad)
4240
.padding(.horizontal, insets: geometry.safeAreaInsets)
4341
Divider()
@@ -46,20 +44,18 @@ struct CouponUsageDetails: View {
4644
TitleAndTextFieldRow(title: Localization.usageLimitPerUser,
4745
placeholder: Localization.unlimited,
4846
text: $viewModel.usageLimitPerUser,
49-
editable: false,
5047
keyboardType: .asciiCapableNumberPad)
5148
.padding(.horizontal, insets: geometry.safeAreaInsets)
5249
Divider()
50+
.padding(.leading, Constants.margin)
51+
.padding(.leading, insets: geometry.safeAreaInsets)
5352
}
5453
.background(Color(.listForeground))
55-
.padding(.bottom, Constants.margin)
5654

5755
VStack(alignment: .leading, spacing: 0) {
58-
Divider()
5956
TitleAndTextFieldRow(title: Localization.limitUsageToXItems,
6057
placeholder: Localization.allQualifyingInCart,
6158
text: $viewModel.limitUsageToXItems,
62-
editable: false,
6359
keyboardType: .asciiCapableNumberPad)
6460
.padding(.horizontal, insets: geometry.safeAreaInsets)
6561
Divider()
@@ -68,62 +64,114 @@ struct CouponUsageDetails: View {
6864
TitleAndValueRow(title: Localization.allowedEmails,
6965
value: viewModel.allowedEmails.isNotEmpty ?
7066
.content(viewModel.allowedEmails) :
71-
.content(Localization.noRestrictions))
72-
.padding(.horizontal, insets: geometry.safeAreaInsets)
67+
.content(Localization.noRestrictions),
68+
selectionStyle: .disclosure) {
69+
showingAllowedEmails = true
70+
}
71+
.padding(.horizontal, insets: geometry.safeAreaInsets)
72+
7373
Divider()
74+
.padding(.leading, Constants.margin)
75+
.padding(.leading, insets: geometry.safeAreaInsets)
7476
}
75-
.background(Color(.listForeground))
76-
.padding(.top, Constants.margin)
7777

78-
ListHeaderView(text: Localization.usageLimits.uppercased(), alignment: .left)
79-
.padding(.horizontal, insets: geometry.safeAreaInsets)
80-
VStack(alignment: .leading, spacing: 0) {
81-
Divider()
82-
TitleAndValueRow(title: Localization.individualUseOnly,
83-
value: .content(viewModel.individualUseOnly ? Localization.yes : Localization.no))
78+
VStack(alignment: .leading, spacing: Constants.verticalSpacing) {
79+
TitleAndToggleRow(title: Localization.individualUseOnly,
80+
isOn: $viewModel.individualUseOnly)
81+
.padding(.horizontal, Constants.margin)
8482
.padding(.horizontal, insets: geometry.safeAreaInsets)
85-
.padding(.vertical, Constants.verticalSpacing)
83+
8684
Divider()
8785
.padding(.leading, Constants.margin)
8886
.padding(.leading, insets: geometry.safeAreaInsets)
89-
TitleAndValueRow(title: Localization.excludeSaleItems,
90-
value: .content(viewModel.excludeSaleItems ? Localization.yes : Localization.no))
87+
88+
Text(Localization.individualUseDescription)
89+
.footnoteStyle()
90+
.padding(.horizontal, Constants.margin)
91+
.padding(.horizontal, insets: geometry.safeAreaInsets)
92+
93+
TitleAndToggleRow(title: Localization.excludeSaleItems,
94+
isOn: $viewModel.excludeSaleItems)
95+
.padding(.horizontal, Constants.margin)
9196
.padding(.horizontal, insets: geometry.safeAreaInsets)
92-
.padding(.vertical, Constants.verticalSpacing)
97+
9398
Divider()
99+
.padding(.leading, Constants.margin)
100+
.padding(.leading, insets: geometry.safeAreaInsets)
101+
102+
Text(Localization.excludeSaleItemsDescription)
103+
.footnoteStyle()
104+
.padding(.horizontal, Constants.margin)
105+
.padding(.horizontal, insets: geometry.safeAreaInsets)
94106
}
95-
.background(Color(.listForeground))
96-
.padding(.bottom, Constants.margin)
107+
.padding(.vertical, Constants.margin)
97108
}
109+
110+
VStack(alignment: .leading, spacing: Constants.margin) {
111+
Text(Localization.exclusions.uppercased())
112+
.footnoteStyle()
113+
Button(action: {
114+
// TODO: show product selection
115+
}) {
116+
HStack {
117+
Image(uiImage: UIImage.plusImage)
118+
.resizable()
119+
.frame(width: Constants.plusIconSize * scale, height: Constants.plusIconSize * scale)
120+
Text(Localization.excludeProducts)
121+
}
122+
}
123+
.buttonStyle(SecondaryButtonStyle(labelFont: .body))
124+
125+
Button(action: {
126+
// TODO: show category selection
127+
}) {
128+
HStack {
129+
Image(uiImage: UIImage.plusImage)
130+
.resizable()
131+
.frame(width: Constants.plusIconSize * scale, height: Constants.plusIconSize * scale)
132+
Text(Localization.excludeProductCategories)
133+
}
134+
}
135+
.buttonStyle(SecondaryButtonStyle(labelFont: .body))
136+
}
137+
.padding(.vertical, Constants.sectionSpacing)
138+
.padding(.horizontal, Constants.margin)
139+
.padding(.horizontal, insets: geometry.safeAreaInsets)
140+
.frame(maxWidth: .infinity, alignment: .leading)
98141
}
99-
.background(Color(.listBackground))
142+
.background(Color(.listForeground))
100143
.ignoresSafeArea(.container, edges: [.horizontal])
144+
145+
LazyNavigationLink(destination: CouponAllowedEmails(emailFormats: $viewModel.allowedEmails), isActive: $showingAllowedEmails) {
146+
EmptyView()
147+
}
101148
}
102-
.navigationTitle(Localization.usageDetails)
149+
.navigationTitle(Localization.usageRestriction)
103150
}
104151
}
105152

106-
private extension CouponUsageDetails {
153+
private extension CouponRestrictions {
107154
enum Constants {
108155
static let margin: CGFloat = 16
109156
static let verticalSpacing: CGFloat = 8
157+
static let sectionSpacing: CGFloat = 30
158+
static let plusIconSize: CGFloat = 16
110159
}
111160

112161
enum Localization {
113-
static let usageDetails = NSLocalizedString("Usage Details", comment: "Navigation title for usage details screen")
114162
static let usageRestriction = NSLocalizedString(
115163
"Usage Restrictions",
116164
comment: "Title for the usage restrictions section on coupon usage details screen"
117165
)
118166
static let minimumSpend = NSLocalizedString(
119-
"Minimum Spend (%1$@)",
167+
"Min. Spend (%1$@)",
120168
comment: "Title for the minimum spend row on coupon usage details screen with currency symbol within the brackets. " +
121-
"Reads like: Minimum Spend ($)"
169+
"Reads like: Min. Spend ($)"
122170
)
123171
static let maximumSpend = NSLocalizedString(
124-
"Maximum Spend (%1$@)",
172+
"Max. Spend (%1$@)",
125173
comment: "Title for the maximum spend row on coupon usage details screen with currency symbol within the brackets. " +
126-
"Reads like: Maximum Spend ($)"
174+
"Reads like: Max. Spend ($)"
127175
)
128176
static let usageLimitPerCoupon = NSLocalizedString(
129177
"Usage Limit Per Coupon",
@@ -141,42 +189,52 @@ private extension CouponUsageDetails {
141189
"Allowed Emails",
142190
comment: "Title for the allowed email row in coupon usage details screen."
143191
)
144-
static let usageLimits = NSLocalizedString("Usage Limits", comment: "Title for the usage limits section on coupon usage details screen")
145192
static let individualUseOnly = NSLocalizedString(
146193
"Individual Use Only",
147194
comment: "Title for the individual use only row in coupon usage details screen."
148195
)
196+
static let individualUseDescription = NSLocalizedString(
197+
"Turn this on if the coupon cannot be used in conjunction with other coupons.",
198+
comment: "Description for the individual use only row in coupon usage details screen."
199+
)
149200
static let excludeSaleItems = NSLocalizedString(
150201
"Exclude Sale Items",
151202
comment: "Title for the exclude sale items row in coupon usage details screen."
152203
)
204+
static let excludeSaleItemsDescription = NSLocalizedString(
205+
"Turn this on if the coupon should not apply to items on sale. " +
206+
"Per-item coupons will only work if the item is not on sale. " +
207+
"Per-cart coupons will only work if there are items in the cart that are not on sale.",
208+
comment: "Description for the exclude sale items row in coupon usage details screen."
209+
)
153210
static let none = NSLocalizedString("None", comment: "Value for fields in Coupon Usage Details screen when no value is set")
154211
static let unlimited = NSLocalizedString("Unlimited", comment: "Value for fields in Coupon Usage Details screen when no limit is set")
155212
static let allQualifyingInCart = NSLocalizedString(
156-
"All Qualifying in Cart",
213+
"All Qualifying",
157214
comment: "Value for the limit usage to X items row in Coupon Usage Details screen when no limit is set"
158215
)
159216
static let noRestrictions = NSLocalizedString(
160217
"No Restrictions",
161218
comment: "Value for the allowed emails row in Coupon Usage Details screen when no restriction is set"
162219
)
163-
static let yes = NSLocalizedString(
164-
"Yes",
165-
comment: "Value for the individual use only row or the exclude sale items row in Coupon Usage Details screen when the value is true"
220+
static let exclusions = NSLocalizedString("Exclusions", comment: "Title of the exclusions section in Coupon Usage Details screen")
221+
static let excludeProducts = NSLocalizedString(
222+
"Exclude Products",
223+
comment: "Title of the action button to add products to the exclusion list in Coupon Usage Details screen"
166224
)
167-
static let no = NSLocalizedString(
168-
"No",
169-
comment: "Value for the individual use only row or the exclude sale items row in Coupon Usage Details screen when the value is false"
225+
static let excludeProductCategories = NSLocalizedString(
226+
"Exclude Product Categories",
227+
comment: "Title of the action button to add product categories to the exclusion list in Coupon Usage Details screen"
170228
)
171229
}
172230
}
173231

174232
#if DEBUG
175-
struct CouponUsageDetails_Previews: PreviewProvider {
233+
struct CouponRestrictions_Previews: PreviewProvider {
176234
static var previews: some View {
177-
CouponUsageDetails(viewModel: .init(coupon: .sampleCoupon))
235+
CouponRestrictions(viewModel: .init(coupon: .sampleCoupon))
178236

179-
CouponUsageDetails(viewModel: .init(coupon: .sampleCoupon))
237+
CouponRestrictions(viewModel: .init(coupon: .sampleCoupon))
180238
.previewLayout(.fixed(width: 715, height: 320))
181239
}
182240
}

WooCommerce/Classes/ViewRelated/Coupons/UsageDetails/CouponUsageDetailsViewModel.swift renamed to WooCommerce/Classes/ViewRelated/Coupons/UsageDetails/CouponRestrictionsViewModel.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import Combine
22
import Foundation
33
import Yosemite
44

5-
/// View Model for `CouponUsageDetails`
5+
/// View Model for `CouponRestriction`
66
///
7-
final class CouponUsageDetailsViewModel: ObservableObject {
7+
final class CouponRestrictionsViewModel: ObservableObject {
88
private let coupon: Coupon
99

1010
let currencySymbol: String

WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ButtonStyles.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ struct PrimaryButtonStyle: ButtonStyle {
1212
}
1313

1414
struct SecondaryButtonStyle: ButtonStyle {
15+
var labelFont: Font = .headline
1516
func makeBody(configuration: Configuration) -> some View {
16-
SecondaryButton(configuration: configuration)
17+
SecondaryButton(configuration: configuration, labelFont: labelFont)
1718
}
1819
}
1920

@@ -136,11 +137,12 @@ private struct SecondaryButton: View {
136137
@Environment(\.isEnabled) var isEnabled
137138

138139
let configuration: ButtonStyleConfiguration
140+
let labelFont: Font
139141

140142
var body: some View {
141143
BaseButton(configuration: configuration)
142144
.foregroundColor(Color(foregroundColor))
143-
.font(.headline)
145+
.font(labelFont)
144146
.background(
145147
RoundedRectangle(cornerRadius: Style.defaultCornerRadius)
146148
.fill(Color(backgroundColor))

0 commit comments

Comments
 (0)