Skip to content

Commit 8f7503b

Browse files
authored
Merge pull request #6186 from woocommerce/issue/5766-coupon-usage-details
Coupons: Usage Details screen (read only)
2 parents 837466f + cb75502 commit 8f7503b

File tree

8 files changed

+316
-13
lines changed

8 files changed

+316
-13
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ struct CouponDetails: View {
2424
@ObservedObject private var viewModel: CouponDetailsViewModel
2525
@State private var showingActionSheet: Bool = false
2626
@State private var showingShareSheet: Bool = false
27+
@State private var showingUsageDetails: Bool = false
2728

2829
/// The presenter to display notice when the coupon code is copied.
2930
/// It is kept internal so that the hosting controller can update its presenting controller to itself.
@@ -140,12 +141,15 @@ struct CouponDetails: View {
140141
Text(Localization.usageDetails)
141142
.bodyStyle()
142143
}, action: {
143-
// TODO-5766: Add usage details screen
144+
showingUsageDetails = true
144145
}).padding(.horizontal, insets: geometry.safeAreaInsets)
145146
}
146147
.background(Color(.listForeground))
147148
Divider()
148149
}
150+
NavigationLink(destination: CouponUsageDetails(viewModel: .init(coupon: viewModel.coupon)), isActive: $showingUsageDetails) {
151+
EmptyView()
152+
}.hidden()
149153
}
150154
.background(Color(.listBackground))
151155
.ignoresSafeArea(.container, edges: [.horizontal, .bottom])

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ final class CouponDetailsViewModel: ObservableObject {
4646

4747
/// The current coupon
4848
///
49-
@Published private var coupon: Coupon {
49+
@Published private(set) var coupon: Coupon {
5050
didSet {
5151
populateDetails()
5252
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import SwiftUI
2+
3+
struct CouponUsageDetails: View {
4+
5+
@ObservedObject private var viewModel: CouponUsageDetailsViewModel
6+
7+
init(viewModel: CouponUsageDetailsViewModel) {
8+
self.viewModel = viewModel
9+
}
10+
11+
var body: some View {
12+
GeometryReader { geometry in
13+
ScrollView {
14+
VStack(alignment: .leading, spacing: 0) {
15+
ListHeaderView(text: Localization.usageRestriction.uppercased(), alignment: .left)
16+
.padding(.horizontal, insets: geometry.safeAreaInsets)
17+
VStack(alignment: .leading, spacing: 0) {
18+
Divider()
19+
TitleAndTextFieldRow(title: String.localizedStringWithFormat(Localization.minimumSpend, viewModel.currencySymbol),
20+
placeholder: Localization.none,
21+
text: $viewModel.minimumSpend,
22+
editable: false,
23+
keyboardType: .decimalPad)
24+
.padding(.horizontal, insets: geometry.safeAreaInsets)
25+
Divider()
26+
.padding(.leading, Constants.margin)
27+
.padding(.leading, insets: geometry.safeAreaInsets)
28+
TitleAndTextFieldRow(title: String.localizedStringWithFormat(Localization.maximumSpend, viewModel.currencySymbol),
29+
placeholder: Localization.none,
30+
text: $viewModel.maximumSpend,
31+
editable: false,
32+
keyboardType: .decimalPad)
33+
.padding(.horizontal, insets: geometry.safeAreaInsets)
34+
Divider()
35+
.padding(.leading, Constants.margin)
36+
.padding(.leading, insets: geometry.safeAreaInsets)
37+
TitleAndTextFieldRow(title: Localization.usageLimitPerCoupon,
38+
placeholder: Localization.unlimited,
39+
text: $viewModel.usageLimitPerCoupon,
40+
editable: false,
41+
keyboardType: .asciiCapableNumberPad)
42+
.padding(.horizontal, insets: geometry.safeAreaInsets)
43+
Divider()
44+
.padding(.leading, Constants.margin)
45+
.padding(.leading, insets: geometry.safeAreaInsets)
46+
TitleAndTextFieldRow(title: Localization.usageLimitPerUser,
47+
placeholder: Localization.unlimited,
48+
text: $viewModel.usageLimitPerUser,
49+
editable: false,
50+
keyboardType: .asciiCapableNumberPad)
51+
.padding(.horizontal, insets: geometry.safeAreaInsets)
52+
Divider()
53+
}
54+
.background(Color(.listForeground))
55+
.padding(.bottom, Constants.margin)
56+
57+
VStack(alignment: .leading, spacing: 0) {
58+
Divider()
59+
TitleAndTextFieldRow(title: Localization.limitUsageToXItems,
60+
placeholder: Localization.allQualifyingInCart,
61+
text: $viewModel.limitUsageToXItems,
62+
editable: false,
63+
keyboardType: .asciiCapableNumberPad)
64+
.padding(.horizontal, insets: geometry.safeAreaInsets)
65+
Divider()
66+
.padding(.leading, Constants.margin)
67+
.padding(.leading, insets: geometry.safeAreaInsets)
68+
TitleAndValueRow(title: Localization.allowedEmails,
69+
value: viewModel.allowedEmails.isNotEmpty ?
70+
.content(viewModel.allowedEmails) :
71+
.content(Localization.noRestrictions))
72+
.padding(.horizontal, insets: geometry.safeAreaInsets)
73+
Divider()
74+
}
75+
.background(Color(.listForeground))
76+
.padding(.top, Constants.margin)
77+
78+
ListHeaderView(text: Localization.usageLimits.uppercased(), alignment: .left)
79+
.padding(.horizontal, insets: geometry.safeAreaInsets)
80+
VStack(alignment: .leading, spacing: 0) {
81+
Divider()
82+
TitleAndToggleRow(title: Localization.individualUseOnly, isOn: .constant(viewModel.individualUseOnly))
83+
.padding(.horizontal, insets: geometry.safeAreaInsets)
84+
.padding(.horizontal, Constants.margin)
85+
.padding(.vertical, Constants.verticalSpacing)
86+
Divider()
87+
.padding(.leading, Constants.margin)
88+
.padding(.leading, insets: geometry.safeAreaInsets)
89+
TitleAndToggleRow(title: Localization.excludeSaleItems, isOn: .constant(viewModel.excludeSaleItems))
90+
.padding(.horizontal, insets: geometry.safeAreaInsets)
91+
.padding(.horizontal, Constants.margin)
92+
.padding(.vertical, Constants.verticalSpacing)
93+
Divider()
94+
}
95+
.background(Color(.listForeground))
96+
.padding(.bottom, Constants.margin)
97+
}
98+
}
99+
.background(Color(.listBackground))
100+
.ignoresSafeArea(.container, edges: [.horizontal])
101+
}
102+
.navigationTitle(Localization.usageDetails)
103+
}
104+
}
105+
106+
private extension CouponUsageDetails {
107+
enum Constants {
108+
static let margin: CGFloat = 16
109+
static let verticalSpacing: CGFloat = 8
110+
}
111+
112+
enum Localization {
113+
static let usageDetails = NSLocalizedString("Usage Details", comment: "Navigation title for usage details screen")
114+
static let usageRestriction = NSLocalizedString(
115+
"Usage Restrictions",
116+
comment: "Title for the usage restrictions section on coupon usage details screen"
117+
)
118+
static let minimumSpend = NSLocalizedString(
119+
"Minimum Spend (%1$@)",
120+
comment: "Title for the minimum spend row on coupon usage details screen with currency symbol within the brackets. " +
121+
"Reads like: Minimum Spend ($)"
122+
)
123+
static let maximumSpend = NSLocalizedString(
124+
"Maximum Spend (%1$@)",
125+
comment: "Title for the maximum spend row on coupon usage details screen with currency symbol within the brackets. " +
126+
"Reads like: Maximum Spend ($)"
127+
)
128+
static let usageLimitPerCoupon = NSLocalizedString(
129+
"Usage Limit Per Coupon",
130+
comment: "Title for the usage limit per coupon row in coupon usage details screen."
131+
)
132+
static let usageLimitPerUser = NSLocalizedString(
133+
"Usage Limit Per User",
134+
comment: "Title for the usage limit per user row in coupon usage details screen."
135+
)
136+
static let limitUsageToXItems = NSLocalizedString(
137+
"Limit Usage to X Items",
138+
comment: "Title for the limit usage to X items row in coupon usage details screen."
139+
)
140+
static let allowedEmails = NSLocalizedString(
141+
"Allowed Emails",
142+
comment: "Title for the allowed email row in coupon usage details screen."
143+
)
144+
static let usageLimits = NSLocalizedString("Usage Limits", comment: "Title for the usage limits section on coupon usage details screen")
145+
static let individualUseOnly = NSLocalizedString(
146+
"Individual Use Only",
147+
comment: "Title for the individual use only row in coupon usage details screen."
148+
)
149+
static let excludeSaleItems = NSLocalizedString(
150+
"Exclude Sale Items",
151+
comment: "Title for the exclude sale items row in coupon usage details screen."
152+
)
153+
static let none = NSLocalizedString("None", comment: "Value for fields in Coupon Usage Details screen when no value is set")
154+
static let unlimited = NSLocalizedString("Unlimited", comment: "Value for fields in Coupon Usage Details screen when no limit is set")
155+
static let allQualifyingInCart = NSLocalizedString(
156+
"All Qualifying in Cart",
157+
comment: "Value for the limit usage to X items row in Coupon Usage Details screen when no limit is set"
158+
)
159+
static let noRestrictions = NSLocalizedString(
160+
"No Restrictions",
161+
comment: "Value for the allowed emails row in Coupon Usage Details screen when no restriction is set"
162+
)
163+
}
164+
}
165+
166+
#if DEBUG
167+
struct CouponUsageDetails_Previews: PreviewProvider {
168+
static var previews: some View {
169+
CouponUsageDetails(viewModel: .init(coupon: .sampleCoupon))
170+
171+
CouponUsageDetails(viewModel: .init(coupon: .sampleCoupon))
172+
.previewLayout(.fixed(width: 715, height: 320))
173+
}
174+
}
175+
#endif
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import Combine
2+
import Foundation
3+
import Yosemite
4+
5+
/// View Model for `CouponUsageDetails`
6+
///
7+
final class CouponUsageDetailsViewModel: ObservableObject {
8+
private let coupon: Coupon
9+
10+
let currencySymbol: String
11+
12+
@Published var minimumSpend: String
13+
14+
@Published var maximumSpend: String
15+
16+
@Published var usageLimitPerCoupon: String
17+
18+
@Published var usageLimitPerUser: String
19+
20+
@Published var limitUsageToXItems: String
21+
22+
@Published var allowedEmails: String
23+
24+
@Published var individualUseOnly: Bool
25+
26+
@Published var excludeSaleItems: Bool
27+
28+
init(coupon: Coupon,
29+
currencySettings: CurrencySettings = ServiceLocator.currencySettings) {
30+
self.coupon = coupon
31+
self.currencySymbol = currencySettings.symbol(from: currencySettings.currencyCode)
32+
33+
self.minimumSpend = coupon.minimumAmount
34+
self.maximumSpend = coupon.maximumAmount
35+
if let perCoupon = coupon.usageLimit {
36+
self.usageLimitPerCoupon = "\(perCoupon)"
37+
} else {
38+
self.usageLimitPerCoupon = ""
39+
}
40+
41+
if let perUser = coupon.usageLimitPerUser {
42+
self.usageLimitPerUser = "\(perUser)"
43+
} else {
44+
self.usageLimitPerUser = ""
45+
}
46+
47+
if let limitUsageItemCount = coupon.limitUsageToXItems {
48+
self.limitUsageToXItems = "\(limitUsageItemCount)"
49+
} else {
50+
self.limitUsageToXItems = ""
51+
}
52+
53+
if coupon.emailRestrictions.isNotEmpty {
54+
self.allowedEmails = coupon.emailRestrictions.joined(separator: ", ")
55+
} else {
56+
self.allowedEmails = ""
57+
}
58+
59+
self.individualUseOnly = coupon.individualUse
60+
self.excludeSaleItems = coupon.excludeSaleItems
61+
}
62+
}

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@ import SwiftUI
33
/// Renders a row with a label on the left side, and a text field on the right side, with eventually a symbol (like $)
44
///
55
struct TitleAndTextFieldRow: View {
6-
let title: String
7-
let placeholder: String
8-
@Binding var text: String
9-
let symbol: String?
10-
let keyboardType: UIKeyboardType
11-
let onEditingChanged: ((Bool) -> Void)?
6+
private let title: String
7+
private let placeholder: String
8+
private let symbol: String?
9+
private let keyboardType: UIKeyboardType
10+
private let onEditingChanged: ((Bool) -> Void)?
11+
private let editable: Bool
12+
13+
@Binding private var text: String
1214

1315
init(title: String,
1416
placeholder: String,
1517
text: Binding<String>,
1618
symbol: String? = nil,
19+
editable: Bool = true,
1720
keyboardType: UIKeyboardType = .default,
1821
onEditingChanged: ((Bool) -> Void)? = nil) {
1922
self.title = title
2023
self.placeholder = placeholder
2124
self._text = text
2225
self.symbol = symbol
26+
self.editable = editable
2327
self.keyboardType = keyboardType
2428
self.onEditingChanged = onEditingChanged
2529
}
@@ -35,6 +39,7 @@ struct TitleAndTextFieldRow: View {
3539
.multilineTextAlignment(.trailing)
3640
.font(.body)
3741
.keyboardType(keyboardType)
42+
.disabled(!editable)
3843
if let symbol = symbol {
3944
Text(symbol)
4045
.bodyStyle()

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,19 @@ struct TitleAndValueRow: View {
1010
case highlight
1111
}
1212

13-
let title: String
14-
let value: Value
15-
var bold: Bool = false
16-
let selectionStyle: SelectionStyle
17-
var action: () -> Void
13+
private let title: String
14+
private let value: Value
15+
private let bold: Bool
16+
private let selectionStyle: SelectionStyle
17+
private let action: () -> Void
18+
19+
init(title: String, value: Value, bold: Bool = false, selectionStyle: SelectionStyle = .none, action: @escaping () -> Void = {}) {
20+
self.title = title
21+
self.value = value
22+
self.bold = bold
23+
self.selectionStyle = selectionStyle
24+
self.action = action
25+
}
1826

1927
var body: some View {
2028
Button(action: {

0 commit comments

Comments
 (0)